feat(MembersOverview): add training participations column toggle and localization updates
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 35s

- Introduced a checkbox in the MembersOverviewSection to toggle the visibility of the training participations column.
- Updated the MembersView to handle the new toggle state and display training participations accordingly.
- Enhanced localization files across multiple languages to include the new term for the training participations column.
- Refactored member age class display logic to improve clarity and maintainability.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 16:28:00 +02:00
parent 8b9a4b7bca
commit 59034ff397
17 changed files with 99 additions and 24 deletions

View File

@@ -58,6 +58,14 @@
>
<span>{{ $t('members.showInactiveMembers') }}</span>
</label>
<label class="checkbox-item">
<input
type="checkbox"
:checked="showTrainingParticipationsColumn"
@change="$emit('update:show-training-participations-column', $event.target.checked)"
>
<span>{{ $t('members.showTrainingParticipationsColumn') }}</span>
</label>
</div>
<div class="filter-group">
<label>{{ $t('members.ttSeasonFilter') }}</label>
@@ -220,6 +228,7 @@ export default {
memberScopeOptions: { type: Array, required: true },
selectedMemberScope: { type: String, required: true },
showInactiveMembers: { type: Boolean, required: true },
showTrainingParticipationsColumn: { type: Boolean, required: true },
selectedAgeGroup: { type: String, required: true },
selectedSeasonStartYear: { type: Number, required: true },
seasonFilterOptions: { type: Array, required: true },
@@ -244,6 +253,7 @@ export default {
'update:search-query',
'update:selected-member-scope',
'update:show-inactive-members',
'update:show-training-participations-column',
'update:selected-age-group',
'update:selected-season-start-year',
'update:selected-age-from',

View File

@@ -466,6 +466,7 @@
"create": "Anlegen",
"clearFields": "Felder leeren",
"showInactiveMembers": "Inaktive Mitglieder anzeigen",
"showTrainingParticipationsColumn": "Spalte „Trainingsteilnahmen“ anzeigen",
"ageGroup": "Altersklasse",
"ttSeasonFilter": "Saison (Stichtag)",
"ttSeasonCurrentTag": "aktuell",

View File

@@ -241,6 +241,7 @@
"create": "Anlegen",
"clearFields": "Felder leeren",
"showInactiveMembers": "Inaktive Mitglieder anzeigen",
"showTrainingParticipationsColumn": "Spalte „Trainingsteilnahmen“ anzeigen",
"ageGroup": "Altersklasse",
"ttSeasonFilter": "Saison (Stichtag)",
"ttSeasonCurrentTag": "aktuell",

View File

@@ -466,6 +466,7 @@
"create": "Create",
"clearFields": "Clear fields",
"showInactiveMembers": "Show inactive members",
"showTrainingParticipationsColumn": "Show “Training participations” column",
"ageGroup": "Age group",
"ttSeasonFilter": "Season (cutoff)",
"ttSeasonCurrentTag": "current",

View File

@@ -741,6 +741,7 @@
"create": "Create",
"clearFields": "Clear fields",
"showInactiveMembers": "Show inactive members",
"showTrainingParticipationsColumn": "Show “Training participations” column",
"ageGroup": "Age group",
"ttSeasonFilter": "Season (cutoff)",
"ttSeasonCurrentTag": "current",

View File

@@ -466,6 +466,7 @@
"create": "Create",
"clearFields": "Clear fields",
"showInactiveMembers": "Show inactive members",
"showTrainingParticipationsColumn": "Show “Training participations” column",
"ageGroup": "Age group",
"ttSeasonFilter": "Season (cutoff)",
"ttSeasonCurrentTag": "current",

View File

@@ -433,6 +433,7 @@
"create": "Crear",
"clearFields": "Vaciar campos",
"showInactiveMembers": "Mostrar miembros inactivos",
"showTrainingParticipationsColumn": "Mostrar columna « Participaciones »",
"ageGroup": "Categoría de edad",
"ttSeasonFilter": "Temporada (corte)",
"ttSeasonCurrentTag": "actual",

View File

@@ -433,6 +433,7 @@
"create": "Lumikha",
"clearFields": "Burahin ang mga field",
"showInactiveMembers": "Ipakita ang mga hindi aktibong miyembro",
"showTrainingParticipationsColumn": "Ipakita ang column na « Training participations »",
"ageGroup": "Pangkat ng edad",
"ttSeasonFilter": "Season (cutoff)",
"ttSeasonCurrentTag": "kasalukuyan",

View File

@@ -433,6 +433,7 @@
"create": "Créer",
"clearFields": "Vider les champs",
"showInactiveMembers": "Afficher les membres inactifs",
"showTrainingParticipationsColumn": "Afficher la colonne « Participations »",
"ageGroup": "Catégorie d'âge",
"ttSeasonFilter": "Saison (date limite)",
"ttSeasonCurrentTag": "actuelle",

View File

@@ -433,6 +433,7 @@
"create": "Crea",
"clearFields": "Svuota campi",
"showInactiveMembers": "Mostra membri inattivi",
"showTrainingParticipationsColumn": "Mostra colonna « Partecipazioni »",
"ageGroup": "Fascia detà",
"ttSeasonFilter": "Stagione (data di riferimento)",
"ttSeasonCurrentTag": "attuale",

View File

@@ -433,6 +433,7 @@
"create": "作成",
"clearFields": "入力欄をクリア",
"showInactiveMembers": "非アクティブメンバーを表示",
"showTrainingParticipationsColumn": "「参加回数」列を表示",
"ageGroup": "年齢区分",
"ttSeasonFilter": "シーズン(基準日)",
"ttSeasonCurrentTag": "今季",

View File

@@ -433,6 +433,7 @@
"create": "Utwórz",
"clearFields": "Wyczyść pola",
"showInactiveMembers": "Pokaż nieaktywnych członków",
"showTrainingParticipationsColumn": "Pokaż kolumnę „Udziały w treningu”",
"ageGroup": "Kategoria wiekowa",
"ttSeasonFilter": "Sezon (termin)",
"ttSeasonCurrentTag": "bieżący",

View File

@@ -433,6 +433,7 @@
"create": "สร้าง",
"clearFields": "ล้างช่องข้อมูล",
"showInactiveMembers": "แสดงสมาชิกที่ไม่ใช้งาน",
"showTrainingParticipationsColumn": "แสดงคอลัมน์ « การเข้าร่วมเทรนนิง »",
"ageGroup": "กลุ่มอายุ",
"ttSeasonFilter": "ฤดูกาล (วันตัดสิทธิ์)",
"ttSeasonCurrentTag": "ปัจจุบัน",

View File

@@ -433,6 +433,7 @@
"create": "Lumikha",
"clearFields": "Burahin ang mga field",
"showInactiveMembers": "Ipakita ang mga hindi aktibong miyembro",
"showTrainingParticipationsColumn": "Ipakita ang column na « Training participations »",
"ageGroup": "Pangkat ng edad",
"ttSeasonFilter": "Season (cutoff)",
"ttSeasonCurrentTag": "kasalukuyan",

View File

@@ -433,6 +433,7 @@
"create": "创建",
"clearFields": "清空字段",
"showInactiveMembers": "显示非活跃成员",
"showTrainingParticipationsColumn": "显示「训练参与」列",
"ageGroup": "年龄组",
"ttSeasonFilter": "赛季(截止日)",
"ttSeasonCurrentTag": "当前",

View File

@@ -26,6 +26,13 @@ export function getStichtagDate(seasonStartYear, classNum) {
return new Date(y, 0, 1);
}
/** @param {object} member */
export function isFemaleGender(member) {
const g = member?.gender;
if (g == null || g === '') return false;
return String(g).toLowerCase() === 'female';
}
function birthDateToTime(birthDate) {
if (!birthDate) return null;
let d;
@@ -98,7 +105,7 @@ export function memberMatchesTtAgeClass(member, filterKey, seasonStartYear) {
}
if (filterKey.startsWith('M')) {
if (member.gender !== 'female') return false;
if (!isFemaleGender(member)) return false;
const want = `J${filterKey.slice(1)}`;
return jClass === want;
}
@@ -113,7 +120,7 @@ export function formatMemberTtAgeClassLabels(member, seasonStartYear) {
const j = getExclusiveJugendClass(member.birthDate, seasonStartYear);
if (j === null) return { primary: null, secondary: null };
if (j === 'adult') return { primary: 'adult', secondary: null };
if (member.gender === 'female') {
if (isFemaleGender(member)) {
const m = `M${j.slice(1)}`;
return { primary: j, secondary: m };
}

View File

@@ -11,6 +11,7 @@
:member-scope-options="memberScopeOptions"
:selected-member-scope="selectedMemberScope"
:show-inactive-members="showInactiveMembers"
:show-training-participations-column="showTrainingParticipationsColumn"
:selected-age-group="selectedAgeGroup"
:selected-season-start-year="selectedSeasonStartYear"
:season-filter-options="ttSeasonFilterOptions"
@@ -33,6 +34,7 @@
@update:search-query="searchQuery = $event"
@update:selected-member-scope="selectedMemberScope = $event"
@update:show-inactive-members="showInactiveMembers = $event"
@update:show-training-participations-column="setShowTrainingParticipationsColumn"
@update:selected-age-group="selectedAgeGroup = $event"
@update:selected-season-start-year="selectedSeasonStartYear = $event"
@update:selected-age-from="selectedAgeFrom = $event"
@@ -336,8 +338,8 @@
<th>{{ $t('members.birthdate') }}</th>
<th>{{ $t('members.age') }}</th>
<th>{{ $t('members.ttAgeClassCol') }}</th>
<th>{{ $t('members.lastTraining') }}</th>
<th v-if="hasTestMembers">{{ $t('members.trainingParticipations') }}</th>
<th>{{ $t('members.previewLastTraining') }}</th>
<th v-if="showTrainingParticipationsColumn">{{ $t('members.trainingParticipations') }}</th>
<th>{{ $t('members.actions') }}</th>
</tr>
</thead>
@@ -421,11 +423,18 @@
</td>
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
<td>{{ getAgeLabel(member.birthDate) }}</td>
<td class="tt-age-class-cell">{{ formatMemberTtAgeClassCell(member) }}</td>
<td class="tt-age-class-cell">
<div
v-for="(line, idx) in formatMemberTtAgeClassLines(member)"
:key="idx"
class="tt-age-class-line"
>
{{ line }}
</div>
</td>
<td>{{ getOptionalFormattedDate(member.lastTraining, 'members.previewNoLastTraining') }}</td>
<td v-if="hasTestMembers">
<span v-if="member.testMembership">{{ member.trainingParticipations || 0 }}</span>
<span v-else>-</span>
<td v-if="showTrainingParticipationsColumn" class="training-participations-cell">
{{ trainingParticipationsDisplay(member) }}
</td>
<td>
<div class="member-actions-cell">
@@ -572,6 +581,7 @@
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import { getSafeErrorMessage, getSafeMessage } from '../utils/errorMessages.js';
import { safeLocalStorage } from '../utils/storage.js';
import {
getSeasonStartYearFromDate,
memberMatchesTtAgeClass,
@@ -887,9 +897,6 @@ export default {
return members;
},
hasTestMembers() {
return this.members.some(member => member.testMembership);
},
availableGroupsForMember() {
if (!Array.isArray(this.trainingGroups) || this.trainingGroups.length === 0) {
@@ -947,6 +954,7 @@ export default {
selectedMemberForImages: null,
testMembership: false,
showInactiveMembers: false,
showTrainingParticipationsColumn: false,
newPicsInInternetAllowed: false,
newMemberFormHandedOver: false,
newAdultReleaseApproved: false,
@@ -980,6 +988,12 @@ export default {
selectedPreviewTrainingGroups: []
}
},
created() {
const v = safeLocalStorage.getItem('membersShowTrainingParticipationsColumn');
if (v === '1') {
this.showTrainingParticipationsColumn = true;
}
},
async mounted() {
await this.loadTrainingGroups();
await this.init();
@@ -1081,7 +1095,10 @@ export default {
async loadTrainingParticipations() {
try {
const response = await apiClient.get(`/training-stats/${this.currentClub}`);
const trainingStats = response.data.members || [];
if (response.status < 200 || response.status >= 300 || !response.data || typeof response.data !== 'object') {
throw new Error('training-stats invalid response');
}
const trainingStats = Array.isArray(response.data.members) ? response.data.members : [];
// Erstelle eine Map für schnellen Zugriff: memberId -> participationTotal
// Speichere sowohl String- als auch Number-Keys, um Typ-Probleme zu vermeiden
@@ -1233,7 +1250,7 @@ export default {
this.getMemberStatusBadges(member).map(badge => badge.label).join(' | '),
this.getFormattedBirthdate(member.birthDate),
this.getAgeLabel(member.birthDate),
this.formatMemberTtAgeClassCell(member),
this.formatMemberTtAgeClassCsv(member),
this.getFormattedPhoneNumbers(member),
this.getFormattedEmails(member)
]);
@@ -2406,18 +2423,34 @@ export default {
: age;
},
formatMemberTtAgeClassCell(member) {
const { primary, secondary } = formatMemberTtAgeClassLabels(member, this.selectedSeasonStartYear);
if (primary === null) {
formatMemberTtAgeClassLines(member) {
const y0 = getSeasonStartYearFromDate();
return [y0, y0 + 1].map((seasonYear) => {
const { primary, secondary } = formatMemberTtAgeClassLabels(member, seasonYear);
const prefix = formatSeasonSlash(seasonYear);
if (primary === null) {
return `${prefix}: `;
}
if (primary === 'adult') {
return `${prefix}: ${this.$t('members.ttAdult')}`;
}
const body = secondary ? `${primary} · ${secondary}` : primary;
return `${prefix}: ${body}`;
});
},
formatMemberTtAgeClassCsv(member) {
return this.formatMemberTtAgeClassLines(member).join(' | ');
},
trainingParticipationsDisplay(member) {
const n = member.trainingParticipations;
if (n == null || Number.isNaN(Number(n))) {
return '';
}
if (primary === 'adult') {
return this.$t('members.ttAdult');
}
if (secondary) {
return `${primary} · ${secondary}`;
}
return primary;
return String(n);
},
setShowTrainingParticipationsColumn(value) {
this.showTrainingParticipationsColumn = !!value;
safeLocalStorage.setItem('membersShowTrainingParticipationsColumn', this.showTrainingParticipationsColumn ? '1' : '0');
},
clearFilters() {
@@ -3348,7 +3381,18 @@ table td {
.tt-age-class-cell {
font-size: 0.88rem;
white-space: nowrap;
line-height: 1.35;
vertical-align: top;
}
.tt-age-class-line + .tt-age-class-line {
margin-top: 0.2rem;
opacity: 0.92;
}
.training-participations-cell {
text-align: right;
font-variant-numeric: tabular-nums;
}
.action-icons-row {