From 59034ff3978da1f1637d578bb380ebb41aa5e69c Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 1 Apr 2026 16:28:00 +0200 Subject: [PATCH] feat(MembersOverview): add training participations column toggle and localization updates - 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. --- .../members/MembersOverviewSection.vue | 10 +++ frontend/src/i18n/locales/de-CH.json | 1 + frontend/src/i18n/locales/de.json | 1 + frontend/src/i18n/locales/en-AU.json | 1 + frontend/src/i18n/locales/en-GB.json | 1 + frontend/src/i18n/locales/en-US.json | 1 + frontend/src/i18n/locales/es.json | 1 + frontend/src/i18n/locales/fil.json | 1 + frontend/src/i18n/locales/fr.json | 1 + frontend/src/i18n/locales/it.json | 1 + frontend/src/i18n/locales/ja.json | 1 + frontend/src/i18n/locales/pl.json | 1 + frontend/src/i18n/locales/th.json | 1 + frontend/src/i18n/locales/tl.json | 1 + frontend/src/i18n/locales/zh.json | 1 + frontend/src/utils/ttAgeClass.js | 11 ++- frontend/src/views/MembersView.vue | 88 ++++++++++++++----- 17 files changed, 99 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/members/MembersOverviewSection.vue b/frontend/src/components/members/MembersOverviewSection.vue index 9607a25e..eec4766d 100644 --- a/frontend/src/components/members/MembersOverviewSection.vue +++ b/frontend/src/components/members/MembersOverviewSection.vue @@ -58,6 +58,14 @@ > {{ $t('members.showInactiveMembers') }} +
@@ -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', diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index c95c50bd..383b4dad 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -466,6 +466,7 @@ "create": "Anlegen", "clearFields": "Felder leeren", "showInactiveMembers": "Inaktive Mitglieder anzeigen", + "showTrainingParticipationsColumn": "Spalte „Trainingsteilnahmen“ anzeigen", "ageGroup": "Altersklasse", "ttSeasonFilter": "Saison (Stichtag)", "ttSeasonCurrentTag": "aktuell", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 81bbb8ac..7482a348 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -241,6 +241,7 @@ "create": "Anlegen", "clearFields": "Felder leeren", "showInactiveMembers": "Inaktive Mitglieder anzeigen", + "showTrainingParticipationsColumn": "Spalte „Trainingsteilnahmen“ anzeigen", "ageGroup": "Altersklasse", "ttSeasonFilter": "Saison (Stichtag)", "ttSeasonCurrentTag": "aktuell", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index 8a9516ff..2458ee0b 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -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", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index 9e37c380..da75969b 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -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", diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index e6892d01..b857fde6 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -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", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index a23dfaae..0786fcb2 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -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", diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index 77dbfb40..738514ec 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -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", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 6f3fd407..3cab3e1f 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -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", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 8fba25f3..7cec8147 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -433,6 +433,7 @@ "create": "Crea", "clearFields": "Svuota campi", "showInactiveMembers": "Mostra membri inattivi", + "showTrainingParticipationsColumn": "Mostra colonna « Partecipazioni »", "ageGroup": "Fascia d’età", "ttSeasonFilter": "Stagione (data di riferimento)", "ttSeasonCurrentTag": "attuale", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index d755aea3..e98547a9 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -433,6 +433,7 @@ "create": "作成", "clearFields": "入力欄をクリア", "showInactiveMembers": "非アクティブメンバーを表示", + "showTrainingParticipationsColumn": "「参加回数」列を表示", "ageGroup": "年齢区分", "ttSeasonFilter": "シーズン(基準日)", "ttSeasonCurrentTag": "今季", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index f0676ad3..be38f837 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -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", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index b9e1936c..02d90b76 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -433,6 +433,7 @@ "create": "สร้าง", "clearFields": "ล้างช่องข้อมูล", "showInactiveMembers": "แสดงสมาชิกที่ไม่ใช้งาน", + "showTrainingParticipationsColumn": "แสดงคอลัมน์ « การเข้าร่วมเทรนนิง »", "ageGroup": "กลุ่มอายุ", "ttSeasonFilter": "ฤดูกาล (วันตัดสิทธิ์)", "ttSeasonCurrentTag": "ปัจจุบัน", diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index 46e18de5..86c43456 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -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", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 8ade7071..4362f5ff 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -433,6 +433,7 @@ "create": "创建", "clearFields": "清空字段", "showInactiveMembers": "显示非活跃成员", + "showTrainingParticipationsColumn": "显示「训练参与」列", "ageGroup": "年龄组", "ttSeasonFilter": "赛季(截止日)", "ttSeasonCurrentTag": "当前", diff --git a/frontend/src/utils/ttAgeClass.js b/frontend/src/utils/ttAgeClass.js index 018a6c7c..b78516c8 100644 --- a/frontend/src/utils/ttAgeClass.js +++ b/frontend/src/utils/ttAgeClass.js @@ -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 }; } diff --git a/frontend/src/views/MembersView.vue b/frontend/src/views/MembersView.vue index 5948b8c8..594beda4 100644 --- a/frontend/src/views/MembersView.vue +++ b/frontend/src/views/MembersView.vue @@ -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 @@ {{ $t('members.birthdate') }} {{ $t('members.age') }} {{ $t('members.ttAgeClassCol') }} - {{ $t('members.lastTraining') }} - {{ $t('members.trainingParticipations') }} + {{ $t('members.previewLastTraining') }} + {{ $t('members.trainingParticipations') }} {{ $t('members.actions') }} @@ -421,11 +423,18 @@ {{ getFormattedBirthdate(member.birthDate) }} {{ getAgeLabel(member.birthDate) }} - {{ formatMemberTtAgeClassCell(member) }} + +
+ {{ line }} +
+ {{ getOptionalFormattedDate(member.lastTraining, 'members.previewNoLastTraining') }} - - {{ member.trainingParticipations || 0 }} - - + + {{ trainingParticipationsDisplay(member) }}
@@ -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 {