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

- Introduced a new filter for last training in the MembersOverviewSection, allowing users to filter members based on their last training date.
- Updated the MembersView to handle the new last training filter state and integrate it into the member sorting and display logic.
- Enhanced localization files across multiple languages to include new terms related to the last training filter and its options.
- Improved member row tooltip to display last training information, enhancing user experience and clarity.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 16:41:54 +02:00
parent 59034ff397
commit 2d43967c81
16 changed files with 165 additions and 12 deletions

View File

@@ -145,6 +145,20 @@
</option>
</select>
</div>
<div class="filter-group">
<label>{{ $t('members.lastTrainingFilter') }}</label>
<select
:value="selectedLastTrainingFilter"
class="filter-select"
@change="$emit('update:selected-last-training-filter', $event.target.value)"
>
<option value="">{{ $t('common.all') }}</option>
<option value="hasDate">{{ $t('members.lastTrainingFilterHasDate') }}</option>
<option value="noDate">{{ $t('members.lastTrainingFilterNoDate') }}</option>
<option value="notInTraining">{{ $t('members.lastTrainingFilterNotInTraining') }}</option>
</select>
<p class="filter-hint filter-hint-tt">{{ $t('members.lastTrainingFilterHint') }}</p>
</div>
<button @click="$emit('clear-filters')" class="btn-clear-filters">{{ $t('members.clearFilters') }}</button>
</div>
</details>
@@ -236,6 +250,7 @@ export default {
selectedAgeTo: { type: [String, Number], required: true },
selectedGender: { type: String, required: true },
selectedTrainingGroup: { type: String, required: true },
selectedLastTrainingFilter: { type: String, required: true },
trainingGroupOptions: { type: Array, required: true },
selectedSort: { type: String, required: true },
sortDirection: { type: String, required: true },
@@ -260,6 +275,7 @@ export default {
'update:selected-age-to',
'update:selected-gender',
'update:selected-training-group',
'update:selected-last-training-filter',
'clear-filters',
'update:selected-sort',
'toggle-sort-direction',

View File

@@ -472,6 +472,12 @@
"ttSeasonCurrentTag": "aktuell",
"ttSeasonNextTag": "kommend",
"ttStichtagHint": "Stichtag 1.1. (DTTB). Jungen: nur J-Klassen. Mädchen: J und M möglich.",
"lastTrainingFilter": "Letztes Training",
"lastTrainingFilterHasDate": "Mit erfasstem Datum",
"lastTrainingFilterNoDate": "Ohne letztes Training",
"lastTrainingFilterNotInTraining": "„Nicht mehr im Training“",
"lastTrainingFilterHint": "In der Tabelle: aktuelle Saison in der AK-Spalte. Alle Details (beide Saisons, letztes Training, ggf. Teilnahmen) beim Überfahren der Zeile.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "AK (TT)",
"ttAdult": "Erwachsene (kein Jugend nach Stichtag)",
"ttFilterGroupJ": "Mädchen & Jungen (gemischt)",

View File

@@ -247,6 +247,12 @@
"ttSeasonCurrentTag": "aktuell",
"ttSeasonNextTag": "kommend",
"ttStichtagHint": "Stichtag 1.1. (DTTB). Jungen: nur J-Klassen. Mädchen: J und M möglich.",
"lastTrainingFilter": "Letztes Training",
"lastTrainingFilterHasDate": "Mit erfasstem Datum",
"lastTrainingFilterNoDate": "Ohne letztes Training",
"lastTrainingFilterNotInTraining": "„Nicht mehr im Training“",
"lastTrainingFilterHint": "In der Tabelle: aktuelle Saison in der AK-Spalte. Alle Details (beide Saisons, letztes Training, ggf. Teilnahmen) beim Überfahren der Zeile.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "AK (TT)",
"ttAdult": "Erwachsene (kein Jugend nach Stichtag)",
"ttFilterGroupJ": "Mädchen & Jungen (gemischt)",

View File

@@ -472,6 +472,12 @@
"ttSeasonCurrentTag": "current",
"ttSeasonNextTag": "upcoming",
"ttStichtagHint": "Cutoff 1 Jan (DTTB). Boys: J classes only. Girls: J and M.",
"lastTrainingFilter": "Last training",
"lastTrainingFilterHasDate": "With a recorded date",
"lastTrainingFilterNoDate": "No last training",
"lastTrainingFilterNotInTraining": "Flagged “no longer training”",
"lastTrainingFilterHint": "Table: current season in the age-class column. Full detail (both seasons, last training, participations if known) on row hover.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Age class (TT)",
"ttAdult": "Adults (not youth by cutoff)",
"ttFilterGroupJ": "Boys & girls (mixed)",

View File

@@ -747,6 +747,12 @@
"ttSeasonCurrentTag": "current",
"ttSeasonNextTag": "upcoming",
"ttStichtagHint": "Cutoff 1 Jan (DTTB). Boys: J classes only. Girls: J and M.",
"lastTrainingFilter": "Last training",
"lastTrainingFilterHasDate": "With a recorded date",
"lastTrainingFilterNoDate": "No last training",
"lastTrainingFilterNotInTraining": "Flagged “no longer training”",
"lastTrainingFilterHint": "Table: current season in the age-class column. Full detail (both seasons, last training, participations if known) on row hover.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Age class (TT)",
"ttAdult": "Adults (not youth by cutoff)",
"ttFilterGroupJ": "Boys & girls (mixed)",

View File

@@ -472,6 +472,12 @@
"ttSeasonCurrentTag": "current",
"ttSeasonNextTag": "upcoming",
"ttStichtagHint": "Cutoff 1 Jan (DTTB). Boys: J classes only. Girls: J and M.",
"lastTrainingFilter": "Last training",
"lastTrainingFilterHasDate": "With a recorded date",
"lastTrainingFilterNoDate": "No last training",
"lastTrainingFilterNotInTraining": "Flagged “no longer training”",
"lastTrainingFilterHint": "Table: current season in the age-class column. Full detail (both seasons, last training, participations if known) on row hover.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Age class (TT)",
"ttAdult": "Adults (not youth by cutoff)",
"ttFilterGroupJ": "Boys & girls (mixed)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "actual",
"ttSeasonNextTag": "próxima",
"ttStichtagHint": "Corte el 1 de enero (DTTB). Niños: solo clases J. Niñas: J y M.",
"lastTrainingFilter": "Último entrenamiento",
"lastTrainingFilterHasDate": "Con fecha registrada",
"lastTrainingFilterNoDate": "Sin último entrenamiento",
"lastTrainingFilterNotInTraining": "« Ya no entrena »",
"lastTrainingFilterHint": "Tabla: temporada actual en la columna de edad. Detalle completo (ambas temporadas, último entrenamiento, participaciones) al pasar el ratón por la fila.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Cat. edad (TT)",
"ttAdult": "Adultos (no juvenil según corte)",
"ttFilterGroupJ": "Niños y niñas (mixto)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "kasalukuyan",
"ttSeasonNextTag": "susunod",
"ttStichtagHint": "Cutoff Enero 1 (DTTB). Lalaki: J lang. Babae: J at M.",
"lastTrainingFilter": "Huling training",
"lastTrainingFilterHasDate": "May petsa",
"lastTrainingFilterNoDate": "Walang huling training",
"lastTrainingFilterNotInTraining": "Flag na “hindi na nagte-training”",
"lastTrainingFilterHint": "Table: kasalukuyang season sa AK. Buong detalye (dalawang season, huling training, participations) kapag-hover sa hilera.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Edad (TT)",
"ttAdult": "Adulto (hindi youth sa cutoff)",
"ttFilterGroupJ": "Babae at lalaki (halo)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "actuelle",
"ttSeasonNextTag": "à venir",
"ttStichtagHint": "Date limite le 1er janv. (DTTB). Garçons : classes J uniquement. Filles : J et M.",
"lastTrainingFilter": "Dernier entraînement",
"lastTrainingFilterHasDate": "Avec date enregistrée",
"lastTrainingFilterNoDate": "Sans dernier entraînement",
"lastTrainingFilterNotInTraining": "« Plus à lentraînement »",
"lastTrainingFilterHint": "Tableau : saison actuelle dans la colonne AK. Détail complet (deux saisons, dernier entraînement, participations) au survol de la ligne.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Classe d'âge (TT)",
"ttAdult": "Adultes (pas jeunes selon la date limite)",
"ttFilterGroupJ": "Filles et garçons (mixte)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "attuale",
"ttSeasonNextTag": "prossima",
"ttStichtagHint": "Data di riferimento 1° gennaio (DTTB). Ragazzi: solo classi J. Ragazze: J e M.",
"lastTrainingFilter": "Ultimo allenamento",
"lastTrainingFilterHasDate": "Con data registrata",
"lastTrainingFilterNoDate": "Senza ultimo allenamento",
"lastTrainingFilterNotInTraining": "« Non più in allenamento »",
"lastTrainingFilterHint": "Tabella: stagione corrente nella colonna AK. Dettaglio completo (entrambe le stagioni, ultimo allenamento, partecipazioni) al passaggio del mouse sulla riga.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Classe (TT)",
"ttAdult": "Adulti (non giovanili secondo la data)",
"ttFilterGroupJ": "Ragazze e ragazzi (misto)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "今季",
"ttSeasonNextTag": "来季",
"ttStichtagHint": "基準日1月1日DTTB。男子Jのみ。女子JとM。",
"lastTrainingFilter": "最終参加",
"lastTrainingFilterHasDate": "日付あり",
"lastTrainingFilterNoDate": "最終参加なし",
"lastTrainingFilterNotInTraining": "「参加なし」フラグ",
"lastTrainingFilterHint": "表AK列は今シーズン。列にマウスを乗せると両シーズン・最終参加・参加回数分かる場合を表示。",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "年齢TT",
"ttAdult": "一般(基準日でジュニアではない)",
"ttFilterGroupJ": "男女混合",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "bieżący",
"ttSeasonNextTag": "następny",
"ttStichtagHint": "Termin 1.01 (DTTB). Chłopcy: tylko klasy J. Dziewczęta: J i M.",
"lastTrainingFilter": "Ostatni trening",
"lastTrainingFilterHasDate": "Z zapisaną datą",
"lastTrainingFilterNoDate": "Bez ostatniego treningu",
"lastTrainingFilterNotInTraining": "„Nie trenuje już”",
"lastTrainingFilterHint": "Tabela: bieżący sezon w kolumnie AK. Pełne szczegóły (oba sezony, ostatni trening, udziały) po najechaniu na wiersz.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Kategoria (TT)",
"ttAdult": "Dorośli (brak juniora wg terminu)",
"ttFilterGroupJ": "Dziewczęta i chłopcy (mieszane)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "ปัจจุบัน",
"ttSeasonNextTag": "ถัดไป",
"ttStichtagHint": "วันตัดสิทธิ์ 1 ม.ค. (DTTB) ชาย: เฉพาะ J หญิง: J และ M",
"lastTrainingFilter": "การเข้าร่วมล่าสุด",
"lastTrainingFilterHasDate": "มีวันที่บันทึก",
"lastTrainingFilterNoDate": "ไม่มีการเข้าร่วมล่าสุด",
"lastTrainingFilterNotInTraining": "ทำเครื่องหมาย «ไม่เข้าร่วมอีกต่อไป»",
"lastTrainingFilterHint": "ตาราง: ฤดูกาลปัจจุบันในคอลัมน์ AK รายละเอียดครบ (สองฤดูกาล, การเข้าร่วมล่าสุด, จำนวนครั้ง) เมื่อชี้ที่แถว",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "รุ่นอายุ (TT)",
"ttAdult": "ผู้ใหญ่ (ไม่ใช่เยาวชนตามวันตัด)",
"ttFilterGroupJ": "ชายและหญิง (รวม)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "kasalukuyan",
"ttSeasonNextTag": "susunod",
"ttStichtagHint": "Cutoff Enero 1 (DTTB). Lalaki: J lang. Babae: J at M.",
"lastTrainingFilter": "Huling training",
"lastTrainingFilterHasDate": "May petsa",
"lastTrainingFilterNoDate": "Walang huling training",
"lastTrainingFilterNotInTraining": "Flag na “hindi na nagte-training”",
"lastTrainingFilterHint": "Table: kasalukuyang season sa AK. Buong detalye (dalawang season, huling training, participations) kapag-hover sa hilera.",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "Edad (TT)",
"ttAdult": "Adulto (hindi youth sa cutoff)",
"ttFilterGroupJ": "Babae at lalaki (halo)",

View File

@@ -439,6 +439,12 @@
"ttSeasonCurrentTag": "当前",
"ttSeasonNextTag": "下一",
"ttStichtagHint": "截止日1月1日DTTB。男孩仅 J 组。女孩J 与 M。",
"lastTrainingFilter": "最近训练",
"lastTrainingFilterHasDate": "有记录日期",
"lastTrainingFilterNoDate": "无最近训练",
"lastTrainingFilterNotInTraining": "标记「不再参加训练」",
"lastTrainingFilterHint": "表格:年龄组列显示当前赛季。悬停行可查看两赛季、最近训练及参与次数(若有)。",
"rowTooltipSeparator": "·",
"ttAgeClassCol": "年龄组TT",
"ttAdult": "成人(按截止日非青年)",
"ttFilterGroupJ": "男女孩(混合)",

View File

@@ -19,6 +19,7 @@
:selected-age-to="selectedAgeTo"
:selected-gender="selectedGender"
:selected-training-group="selectedTrainingGroup"
:selected-last-training-filter="selectedLastTrainingFilter"
:training-group-options="trainingGroupFilterOptions"
:selected-sort="selectedSort"
:sort-direction="sortDirection"
@@ -41,6 +42,7 @@
@update:selected-age-to="selectedAgeTo = $event"
@update:selected-gender="selectedGender = $event"
@update:selected-training-group="selectedTrainingGroup = $event"
@update:selected-last-training-filter="selectedLastTrainingFilter = $event"
@clear-filters="clearFilters"
@update:selected-sort="selectedSort = $event"
@toggle-sort-direction="toggleSortDirection"
@@ -345,7 +347,12 @@
</thead>
<tbody>
<template v-for="member in sortedFilteredMembers" :key="member.id">
<tr class="member-row" :class="{ 'row-selected': selectedMemberPreview && selectedMemberPreview.id === member.id, 'row-inactive': !member.active, 'row-test': member.testMembership && !member.memberFormHandedOver, 'row-test-form': member.testMembership && member.memberFormHandedOver }" @click="selectMember(member)">
<tr
class="member-row"
:class="{ 'row-selected': selectedMemberPreview && selectedMemberPreview.id === member.id, 'row-inactive': !member.active, 'row-test': member.testMembership && !member.memberFormHandedOver, 'row-test-form': member.testMembership && member.memberFormHandedOver }"
:title="memberRowHoverTooltip(member)"
@click="selectMember(member)"
>
<td>
<div @click.stop="openImageModal(member)">
<img
@@ -424,15 +431,11 @@
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
<td>{{ getAgeLabel(member.birthDate) }}</td>
<td class="tt-age-class-cell">
<div
v-for="(line, idx) in formatMemberTtAgeClassLines(member)"
:key="idx"
class="tt-age-class-line"
>
{{ line }}
</div>
<span class="tt-age-class-compact">{{ formatMemberTtAgeClassCompactLine(member) }}</span>
</td>
<td class="last-training-cell">
<span class="last-training-compact">{{ getOptionalFormattedDate(member.lastTraining, 'members.previewNoLastTraining') }}</span>
</td>
<td>{{ getOptionalFormattedDate(member.lastTraining, 'members.previewNoLastTraining') }}</td>
<td v-if="showTrainingParticipationsColumn" class="training-participations-cell">
{{ trainingParticipationsDisplay(member) }}
</td>
@@ -736,6 +739,16 @@ export default {
}
}
if (this.selectedLastTrainingFilter === 'hasDate' && !this.hasValidLastTrainingDate(member)) {
return false;
}
if (this.selectedLastTrainingFilter === 'noDate' && this.hasValidLastTrainingDate(member)) {
return false;
}
if (this.selectedLastTrainingFilter === 'notInTraining' && !member.notInTraining) {
return false;
}
if (search) {
const haystack = [
member.firstName,
@@ -977,6 +990,7 @@ export default {
selectedAgeTo: '',
selectedGender: '',
selectedTrainingGroup: '',
selectedLastTrainingFilter: '',
clickTtPendingMemberIds: [],
searchQuery: '',
selectedMemberScope: 'active',
@@ -2423,6 +2437,29 @@ export default {
: age;
},
hasValidLastTrainingDate(member) {
if (!member?.lastTraining) return false;
const d = new Date(member.lastTraining);
return !Number.isNaN(d.getTime());
},
formatMemberTtAgeClassCompactLine(member) {
const lines = this.formatMemberTtAgeClassLines(member);
return lines[0] || '';
},
memberRowHoverTooltip(member) {
const akFull = this.formatMemberTtAgeClassLines(member).join(` ${this.$t('members.rowTooltipSeparator')} `);
const dateLabel = this.$t('members.previewLastTraining');
const dateVal = this.getOptionalFormattedDate(member.lastTraining, 'members.previewNoLastTraining');
const parts = [`${this.$t('members.ttAgeClassCol')}: ${akFull}`, `${dateLabel}: ${dateVal}`];
if (member.notInTraining) {
parts.push(this.$t('members.notInTrainingTooltip', { weeks: member.missedTrainingWeeks || 0 }));
}
const tp = member.trainingParticipations;
if (tp != null && !Number.isNaN(Number(tp))) {
parts.push(`${this.$t('members.trainingParticipations')}: ${tp}`);
}
return parts.join(` ${this.$t('members.rowTooltipSeparator')} `);
},
formatMemberTtAgeClassLines(member) {
const y0 = getSeasonStartYearFromDate();
return [y0, y0 + 1].map((seasonYear) => {
@@ -2460,6 +2497,7 @@ export default {
this.selectedAgeTo = '';
this.selectedGender = '';
this.selectedTrainingGroup = '';
this.selectedLastTrainingFilter = '';
this.showInactiveMembers = false;
this.searchQuery = '';
this.selectedMemberScope = 'active';
@@ -3383,11 +3421,26 @@ table td {
font-size: 0.88rem;
line-height: 1.35;
vertical-align: top;
max-width: 11rem;
}
.tt-age-class-line + .tt-age-class-line {
margin-top: 0.2rem;
opacity: 0.92;
.tt-age-class-compact {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last-training-cell {
max-width: 9rem;
vertical-align: top;
}
.last-training-compact {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.training-participations-cell {