feat(MemberService, SchedulerService): implement inactive member cleanup functionality

- Added a new method in MemberService to clean up inactive members who haven't trained in the last specified days, enhancing data management.
- Integrated the cleanup method into SchedulerService to run automatically via a scheduled job, improving system maintenance.
- Updated the MembersView to reflect changes in member data, ensuring a consistent user experience.
This commit is contained in:
Torsten Schulz (local)
2026-03-18 18:03:15 +01:00
parent eea3372057
commit 9340ee3509
3 changed files with 318 additions and 103 deletions

View File

@@ -54,112 +54,134 @@
{{ membersLoadError }}
</div>
<div v-if="selectedMemberPreview" class="member-preview-panel">
<div class="member-preview-header">
<div>
<div class="member-preview-label">{{ $t('members.memberDetails') }}</div>
<h3 class="member-preview-name">{{ selectedMemberPreview.firstName }} {{ selectedMemberPreview.lastName }}</h3>
</div>
<div class="member-preview-actions">
<button type="button" class="btn-primary" @click="editMember(selectedMemberPreview)">{{ $t('members.editMember') }}</button>
<button type="button" @click="openImageModal(selectedMemberPreview)">{{ $t('members.memberImages') }}</button>
<button type="button" @click="openNotesModal(selectedMemberPreview)">{{ $t('members.notes') }}</button>
<button type="button" @click="openActivitiesModal(selectedMemberPreview)">{{ $t('members.exercises') }}</button>
</div>
</div>
<div class="member-preview-grid">
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.status') }}</span>
<div class="member-status-stack">
<span
v-for="badge in getMemberStatusBadges(selectedMemberPreview)"
:key="badge.key"
class="member-status-badge"
:class="`member-status-badge-${badge.tone}`"
>
{{ badge.label }}
</span>
</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.contact') }}</span>
<div>{{ getFormattedPhoneNumbers(selectedMemberPreview) }}</div>
<div>{{ getFormattedEmails(selectedMemberPreview) }}</div>
<div class="member-preview-muted">{{ getCompactAddress(selectedMemberPreview) || $t('members.noAddressShort') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.birthdate') }}</span>
<div>{{ getFormattedBirthdate(selectedMemberPreview.birthDate) }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.previewLastTraining') }}</span>
<div>{{ getOptionalFormattedDate(selectedMemberPreview.lastTraining, 'members.previewNoLastTraining') }}</div>
<div class="member-preview-muted" v-if="selectedMemberPreview.notInTraining">{{ $t('members.notInTrainingTooltip', { weeks: selectedMemberPreview.missedTrainingWeeks || 0 }) }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.trainingGroups') }}</span>
<div v-if="selectedPreviewTrainingGroups.length" class="member-preview-tags">
<span
v-for="group in selectedPreviewTrainingGroups"
:key="group.id"
class="member-preview-tag"
>
{{ group.name }}
</span>
</div>
<div v-else class="member-preview-muted">{{ $t('members.noGroupsAssigned') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.dataQuality') }}</span>
<div v-if="getMemberDataQualityIssues(selectedMemberPreview).length" class="member-preview-tags">
<span
v-for="issue in getMemberDataQualityIssues(selectedMemberPreview)"
:key="issue.key"
class="member-preview-tag member-preview-tag-warning"
>
{{ issue.label }}
</span>
</div>
<div v-else class="member-preview-muted">{{ $t('members.dataQualityComplete') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.openTasks') }}</span>
<div v-if="getMemberOpenTasks(selectedMemberPreview).length" class="member-preview-tasklist">
<div
v-for="task in getMemberOpenTasks(selectedMemberPreview)"
:key="task.key"
class="member-preview-task"
>
<span>{{ task.label }}</span>
<button
v-if="task.action"
type="button"
class="member-preview-task-action"
@click="runPreviewTask(task.action, selectedMemberPreview)"
>
{{ task.actionLabel }}
</button>
</div>
</div>
<div v-else class="member-preview-muted">{{ $t('members.noOpenTasks') }}</div>
</div>
</div>
</div>
<div class="newmember" v-if="memberFormIsOpen">
<div class="toggle-new-member">
<div class="member-editor-heading">
<span class="add">{{ memberToEdit === null ? '+' : '✎' }}</span>
<BaseDialog
:model-value="!!selectedMemberPreview"
title="Mitgliedsdetails"
size="large"
:width="1080"
max-width="95vw"
@update:model-value="!$event && closeMemberPreviewDialog()"
@close="closeMemberPreviewDialog"
>
<div v-if="selectedMemberPreview" class="member-preview-panel">
<div class="member-preview-header">
<div>
<div class="member-editor-title">{{ memberToEdit === null ? $t('members.newMember') : $t('members.editMember') }}</div>
<div class="member-editor-subtitle">
{{ memberToEdit === null ? $t('members.editorCreateHint') : $t('members.editorEditHint', { name: `${memberToEdit.firstName} ${memberToEdit.lastName}` }) }}
</div>
<div class="member-preview-label">{{ $t('members.memberDetails') }}</div>
<h3 class="member-preview-name">{{ selectedMemberPreview.firstName }} {{ selectedMemberPreview.lastName }}</h3>
</div>
<div class="member-preview-actions">
<button type="button" class="btn-primary" @click="editMember(selectedMemberPreview)">{{ $t('members.editMember') }}</button>
<button type="button" @click="openImageModal(selectedMemberPreview)">{{ $t('members.memberImages') }}</button>
<button type="button" @click="openNotesModal(selectedMemberPreview)">{{ $t('members.notes') }}</button>
<button type="button" @click="openActivitiesModal(selectedMemberPreview)">{{ $t('members.exercises') }}</button>
</div>
</div>
<div class="member-preview-grid">
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.status') }}</span>
<div class="member-status-stack">
<span
v-for="badge in getMemberStatusBadges(selectedMemberPreview)"
:key="badge.key"
class="member-status-badge"
:class="`member-status-badge-${badge.tone}`"
>
{{ badge.label }}
</span>
</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.contact') }}</span>
<div>{{ getFormattedPhoneNumbers(selectedMemberPreview) }}</div>
<div>{{ getFormattedEmails(selectedMemberPreview) }}</div>
<div class="member-preview-muted">{{ getCompactAddress(selectedMemberPreview) || $t('members.noAddressShort') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.birthdate') }}</span>
<div>{{ getFormattedBirthdate(selectedMemberPreview.birthDate) }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.previewLastTraining') }}</span>
<div>{{ getOptionalFormattedDate(selectedMemberPreview.lastTraining, 'members.previewNoLastTraining') }}</div>
<div class="member-preview-muted" v-if="selectedMemberPreview.notInTraining">{{ $t('members.notInTrainingTooltip', { weeks: selectedMemberPreview.missedTrainingWeeks || 0 }) }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.trainingGroups') }}</span>
<div v-if="selectedPreviewTrainingGroups.length" class="member-preview-tags">
<span
v-for="group in selectedPreviewTrainingGroups"
:key="group.id"
class="member-preview-tag"
>
{{ group.name }}
</span>
</div>
<div v-else class="member-preview-muted">{{ $t('members.noGroupsAssigned') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.dataQuality') }}</span>
<div v-if="getMemberDataQualityIssues(selectedMemberPreview).length" class="member-preview-tags">
<span
v-for="issue in getMemberDataQualityIssues(selectedMemberPreview)"
:key="issue.key"
class="member-preview-tag member-preview-tag-warning"
>
{{ issue.label }}
</span>
</div>
<div v-else class="member-preview-muted">{{ $t('members.dataQualityComplete') }}</div>
</div>
<div class="member-preview-item">
<span class="member-preview-item-label">{{ $t('members.openTasks') }}</span>
<div v-if="getMemberOpenTasks(selectedMemberPreview).length" class="member-preview-tasklist">
<div
v-for="task in getMemberOpenTasks(selectedMemberPreview)"
:key="task.key"
class="member-preview-task"
>
<span>{{ task.label }}</span>
<button
v-if="task.action"
type="button"
class="member-preview-task-action"
@click="runPreviewTask(task.action, selectedMemberPreview)"
>
{{ task.actionLabel }}
</button>
</div>
</div>
<div v-else class="member-preview-muted">{{ $t('members.noOpenTasks') }}</div>
</div>
</div>
<button v-if="memberToEdit !== null" @click="resetToNewMember">{{ $t('members.createNewMember') }}</button>
</div>
<div class="new-member-form">
<template #footer>
<button type="button" class="cancel-action" @click="closeMemberPreviewDialog">{{ $t('common.close') }}</button>
</template>
</BaseDialog>
<BaseDialog
:model-value="memberFormIsOpen"
:title="memberToEdit === null ? $t('members.newMember') : $t('members.editMember')"
size="large"
:width="1120"
max-width="95vw"
@update:model-value="!$event && closeMemberFormDialog()"
@close="closeMemberFormDialog"
>
<div class="newmember" v-if="memberFormIsOpen">
<div class="toggle-new-member">
<div class="member-editor-heading">
<span class="add">{{ memberToEdit === null ? '+' : '✎' }}</span>
<div>
<div class="member-editor-title">{{ memberToEdit === null ? $t('members.newMember') : $t('members.editMember') }}</div>
<div class="member-editor-subtitle">
{{ memberToEdit === null ? $t('members.editorCreateHint') : $t('members.editorEditHint', { name: `${memberToEdit.firstName} ${memberToEdit.lastName}` }) }}
</div>
</div>
</div>
<button v-if="memberToEdit !== null" @click="resetToNewMember">{{ $t('members.createNewMember') }}</button>
</div>
<div class="new-member-form">
<label><span>{{ $t('members.firstName') }}:</span> <input type="text" v-model="newFirstname"></label>
<label><span>{{ $t('members.lastName') }}:</span> <input type="text" v-model="newLastname"></label>
<label><span>{{ $t('members.street') }}:</span> <input type="text" v-model="newStreet"></label>
@@ -268,8 +290,9 @@
<button @click="addNewMember">{{ memberToEdit ? $t('members.change') : $t('members.create') }}</button>
<button @click="resetNewMember" v-if="memberToEdit === null" class="cancel-action">{{ $t('members.clearFields') }}</button>
</div>
</div>
</div>
</div>
</BaseDialog>
<div class="members-table-panel">
<div class="members-table-wrap">
<table>
@@ -953,6 +976,10 @@ export default {
this.selectedPreviewTrainingGroups = [];
}
},
closeMemberPreviewDialog() {
this.selectedMemberPreview = null;
this.selectedPreviewTrainingGroups = [];
},
async quickRemoveTestMembershipInternal(member, { silent = false } = {}) {
const response = await apiClient.post(`/clubmembers/quick-update-test-membership/${this.currentClub}/${member.id}`);
if (!response.data.success) {
@@ -1211,6 +1238,10 @@ export default {
this.resetToNewMember();
}
},
closeMemberFormDialog() {
this.memberFormIsOpen = false;
this.resetToNewMember();
},
toggleMemberInfo() {
this.showMemberInfo = !this.showMemberInfo;
@@ -1482,6 +1513,7 @@ export default {
},
async editMember(member) {
const birthDate = member.birthDate;
this.closeMemberPreviewDialog();
this.memberToEdit = member;
this.memberFormIsOpen = true;
this.newFirstname = member.firstName;