feat(TournamentStats): enhance internal tournament statistics with age class filtering
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s

- Updated the `getInternalTournamentPlayerStats` endpoint to accept age class keys for more granular statistics.
- Introduced new utility functions for handling age class filtering in the internal tournament stats service.
- Enhanced the InternalTournamentStats component with a new age class filter UI, allowing users to select specific age classes for their statistics.
- Updated localization strings across multiple languages to support the new age class filtering feature, improving user accessibility and understanding.
This commit is contained in:
Torsten Schulz (local)
2026-04-08 12:50:20 +02:00
parent c1b8b2c665
commit 27f8186d91
19 changed files with 413 additions and 6 deletions

View File

@@ -23,7 +23,7 @@
</button>
<label class="period-label">
<span>{{ $t('tournaments.internalStatsPeriod') }}</span>
<select v-model="months" @change="load" :disabled="loading">
<select v-model="months" @change="onMonthsChange" :disabled="loading">
<option :value="12">{{ $t('tournaments.internalStatsLast12Months') }}</option>
<option :value="6">{{ $t('tournaments.internalStatsLast6Months') }}</option>
<option :value="3">{{ $t('tournaments.internalStatsLast3Months') }}</option>
@@ -35,6 +35,29 @@
</p>
<p class="stats-explain">{{ $t('tournaments.internalStatsPointsExplain') }}</p>
<fieldset v-if="sortedAgeOptions.length > 1" class="age-filter">
<legend class="age-filter-legend">{{ $t('tournaments.internalStatsAgeFilter') }}</legend>
<div class="age-filter-actions">
<button type="button" class="age-filter-link" @click="selectAllAgeKeys" :disabled="loading">
{{ $t('tournaments.internalStatsAgeSelectAll') }}
</button>
<button type="button" class="age-filter-link" @click="selectNoAgeKeys" :disabled="loading">
{{ $t('tournaments.internalStatsAgeSelectNone') }}
</button>
</div>
<div class="age-filter-checkboxes">
<label v-for="opt in sortedAgeOptions" :key="opt.key" class="age-filter-item">
<input
type="checkbox"
:checked="selectedAgeKeys.includes(opt.key)"
:disabled="loading"
@change="onAgeCheckboxChange(opt.key, $event.target.checked)"
/>
<span>{{ formatAgeOption(opt) }}</span>
</label>
</div>
</fieldset>
<div v-if="loading" class="stats-loading">{{ $t('common.loading') }}</div>
<div v-else-if="error" class="stats-error">{{ error }}</div>
<div v-else class="stats-grid">
@@ -108,9 +131,13 @@ export default {
dialogPosition: { x: 80, y: 80 },
stats: {
tournamentCount: 0,
ageClassOptions: [],
absoluteRanking: [],
averageRanking: [],
},
selectedAgeKeys: [],
ageFilterInitialized: false,
pendingResetAgeSelection: false,
};
},
computed: {
@@ -124,9 +151,21 @@ export default {
(!this.stats.absoluteRanking?.length && !this.stats.averageRanking?.length)
);
},
sortedAgeOptions() {
const list = [...(this.stats.ageClassOptions || [])];
list.sort((a, b) =>
this.formatAgeOption(a).localeCompare(this.formatAgeOption(b), 'de', {
sensitivity: 'base',
}),
);
return list;
},
},
watch: {
clubId() {
this.ageFilterInitialized = false;
this.selectedAgeKeys = [];
this.pendingResetAgeSelection = false;
if (this.modelValue) this.load();
},
modelValue(open) {
@@ -154,6 +193,74 @@ export default {
: 'internalStatsLast3Months';
return this.$t(`tournaments.${key}`);
},
buildStatsParams() {
const params = { months: this.months };
if (this.pendingResetAgeSelection) return params;
const opts = this.stats.ageClassOptions || [];
const allKeys = opts.map((o) => o.key);
if (allKeys.length === 0) return params;
if (this.selectedAgeKeys.length === 0) {
params.ageClassKeys = '';
} else if (this.selectedAgeKeys.length < allKeys.length) {
params.ageClassKeys = this.selectedAgeKeys.join(',');
}
return params;
},
onMonthsChange() {
this.pendingResetAgeSelection = true;
this.load();
},
onAgeCheckboxChange(key, checked) {
if (checked) {
if (!this.selectedAgeKeys.includes(key)) {
this.selectedAgeKeys = [...this.selectedAgeKeys, key];
}
} else {
this.selectedAgeKeys = this.selectedAgeKeys.filter((k) => k !== key);
}
this.load();
},
selectAllAgeKeys() {
this.selectedAgeKeys = (this.stats.ageClassOptions || []).map((o) => o.key);
this.load();
},
selectNoAgeKeys() {
this.selectedAgeKeys = [];
this.load();
},
formatAgeOption(opt) {
if (!opt) return '';
if (opt.isNoClass) return this.$t('tournaments.internalStatsAgeNoClass');
const bits = [];
if (opt.name) bits.push(opt.name);
const min = opt.minBirthYear;
const max = opt.maxBirthYear;
if (min != null && max != null) bits.push(`${min}${max}`);
else if (max != null) bits.push(`${max}`);
else if (min != null) bits.push(`${min}`);
if (opt.gender === 'male') bits.push(this.$t('members.genderMale'));
else if (opt.gender === 'female') bits.push(this.$t('members.genderFemale'));
else if (opt.gender === 'mixed') bits.push(this.$t('tournaments.genderMixed'));
return bits.join(' · ');
},
ageFilterPdfLine() {
const opts = this.stats.ageClassOptions || [];
if (opts.length <= 1) return '';
const allKeys = opts.map((o) => o.key);
if (this.selectedAgeKeys.length === allKeys.length) {
return this.$t('tournaments.internalStatsAgeFilterAll');
}
if (this.selectedAgeKeys.length === 0) {
return this.$t('tournaments.internalStatsAgeFilterNone');
}
const labels = this.selectedAgeKeys
.map((k) => {
const o = opts.find((x) => x.key === k);
return o ? this.formatAgeOption(o) : k;
})
.join('; ');
return `${this.$t('tournaments.internalStatsAgeFilter')}: ${labels}`;
},
exportPdf() {
const t = this.$t;
const margin = 14;
@@ -178,6 +285,12 @@ export default {
);
y += 6;
const ageLine = this.ageFilterPdfLine();
if (ageLine) {
pdf.text(ageLine, margin, y);
y += 5;
}
const explain = t('tournaments.internalStatsPointsExplain');
const explainLines = pdf.splitTextToSize(explain, pageW - margin * 2);
pdf.setFontSize(8);
@@ -283,12 +396,26 @@ export default {
this.error = null;
try {
const res = await apiClient.get(`/tournament/internal-stats/${this.clubId}`, {
params: { months: this.months },
params: this.buildStatsParams(),
});
this.stats = res.data || {};
const opts = this.stats.ageClassOptions || [];
if (this.pendingResetAgeSelection) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.pendingResetAgeSelection = false;
this.ageFilterInitialized = true;
} else if (!this.ageFilterInitialized && opts.length) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.ageFilterInitialized = true;
}
} catch (e) {
this.error = e?.response?.data?.error || e.message || 'Error';
this.stats = { tournamentCount: 0, absoluteRanking: [], averageRanking: [] };
this.stats = {
tournamentCount: 0,
ageClassOptions: [],
absoluteRanking: [],
averageRanking: [],
};
} finally {
this.loading = false;
}
@@ -360,6 +487,69 @@ export default {
line-height: 1.45;
}
.age-filter {
margin: 0 0 1rem;
padding: 0.65rem 0.75rem;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
background: #fafafa;
}
.age-filter-legend {
padding: 0 0.35rem;
font-size: 0.9rem;
font-weight: 600;
color: #374151;
}
.age-filter-actions {
display: flex;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.age-filter-link {
background: none;
border: none;
padding: 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--primary-color, #2563eb);
cursor: pointer;
text-decoration: underline;
}
.age-filter-link:hover:not(:disabled) {
color: var(--primary-hover, #1d4ed8);
}
.age-filter-link:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.age-filter-checkboxes {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 11rem;
overflow-y: auto;
}
.age-filter-item {
display: flex;
align-items: flex-start;
gap: 0.45rem;
font-size: 0.88rem;
line-height: 1.35;
cursor: pointer;
}
.age-filter-item input {
margin-top: 0.2rem;
flex-shrink: 0;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohni Klassäzuordnig",
"internalStatsAgeSelectAll": "Alli",
"internalStatsAgeSelectNone": "Keini",
"internalStatsAgeFilterAll": "Alli Altersklassä",
"internalStatsAgeFilterNone": "Kei Altersklass usgwählt",
"internalStatsPeriod": "Ziitruum",
"internalStatsLast12Months": "Letscht 12 Mönet",
"internalStatsLast6Months": "Letscht 6 Mönet",

View File

@@ -409,6 +409,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",
"internalStatsAgeFilterAll": "Alle Altersklassen",
"internalStatsAgeFilterNone": "Keine Altersklasse ausgewählt",
"internalStatsPeriod": "Zeitraum",
"internalStatsLast12Months": "Letzte 12 Monate",
"internalStatsLast6Months": "Letzte 6 Monate",

View File

@@ -701,6 +701,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",
"internalStatsAgeFilterAll": "Alle Altersklassen",
"internalStatsAgeFilterNone": "Keine Altersklasse ausgewählt",
"internalStatsPeriod": "Zeitraum",
"internalStatsLast12Months": "Letzte 12 Monate",
"internalStatsLast6Months": "Letzte 6 Monate",

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -359,6 +359,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Estadísticas de torneos internos (individual)",
"internalStatsOpenButton": "Estadísticas de torneos (individual)",
"internalStatsExportPdf": "Exportar como PDF",
"internalStatsAgeFilter": "Categorías de edad (individual)",
"internalStatsAgeNoClass": "Sin clase asignada",
"internalStatsAgeSelectAll": "Todas",
"internalStatsAgeSelectNone": "Ninguna",
"internalStatsAgeFilterAll": "Todas las categorías",
"internalStatsAgeFilterNone": "Ninguna categoría seleccionada",
"internalStatsPeriod": "Periodo",
"internalStatsLast12Months": "Últimos 12 meses",
"internalStatsLast6Months": "Últimos 6 meses",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Estadistika ng internal na paligsahan (singles)",
"internalStatsOpenButton": "Buksan ang estadistika (singles)",
"internalStatsExportPdf": "I-export bilang PDF",
"internalStatsAgeFilter": "Mga pangkat ng edad (singles)",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",
"internalStatsAgeFilterAll": "Lahat ng pangkat ng edad",
"internalStatsAgeFilterNone": "Walang napiling pangkat ng edad",
"internalStatsPeriod": "Saklaw",
"internalStatsLast12Months": "Huling 12 buwan",
"internalStatsLast6Months": "Huling 6 na buwan",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statistiques des tournois internes (simple)",
"internalStatsOpenButton": "Statistiques (simple)",
"internalStatsExportPdf": "Exporter en PDF",
"internalStatsAgeFilter": "Catégories dâge (simple)",
"internalStatsAgeNoClass": "Sans classe assignée",
"internalStatsAgeSelectAll": "Tout",
"internalStatsAgeSelectNone": "Aucune",
"internalStatsAgeFilterAll": "Toutes les catégories",
"internalStatsAgeFilterNone": "Aucune catégorie sélectionnée",
"internalStatsPeriod": "Période",
"internalStatsLast12Months": "12 derniers mois",
"internalStatsLast6Months": "6 derniers mois",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statistiche tornei interni (singolo)",
"internalStatsOpenButton": "Statistiche tornei (singolo)",
"internalStatsExportPdf": "Esporta PDF",
"internalStatsAgeFilter": "Categorie di età (singolo)",
"internalStatsAgeNoClass": "Senza classe assegnata",
"internalStatsAgeSelectAll": "Tutte",
"internalStatsAgeSelectNone": "Nessuna",
"internalStatsAgeFilterAll": "Tutte le categorie",
"internalStatsAgeFilterNone": "Nessuna categoria selezionata",
"internalStatsPeriod": "Periodo",
"internalStatsLast12Months": "Ultimi 12 mesi",
"internalStatsLast6Months": "Ultimi 6 mesi",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "内部大会の統計(シングルス)",
"internalStatsOpenButton": "統計を表示(シングルス)",
"internalStatsExportPdf": "PDFで出力",
"internalStatsAgeFilter": "年齢クラス(シングルス)",
"internalStatsAgeNoClass": "クラス未設定",
"internalStatsAgeSelectAll": "すべて",
"internalStatsAgeSelectNone": "なし",
"internalStatsAgeFilterAll": "すべての年齢クラス",
"internalStatsAgeFilterNone": "年齢クラス未選択",
"internalStatsPeriod": "期間",
"internalStatsLast12Months": "過去12か月",
"internalStatsLast6Months": "過去6か月",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statystyki turniejów wewnętrznych (singel)",
"internalStatsOpenButton": "Statystyki turniejów (singel)",
"internalStatsExportPdf": "Eksportuj do PDF",
"internalStatsAgeFilter": "Klasy wiekowe (singel)",
"internalStatsAgeNoClass": "Bez przypisania do klasy",
"internalStatsAgeSelectAll": "Wszystkie",
"internalStatsAgeSelectNone": "Żadna",
"internalStatsAgeFilterAll": "Wszystkie klasy wiekowe",
"internalStatsAgeFilterNone": "Nie wybrano klasy wiekowej",
"internalStatsPeriod": "Okres",
"internalStatsLast12Months": "Ostatnie 12 miesięcy",
"internalStatsLast6Months": "Ostatnie 6 miesięcy",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "สถิติการแข่งขันภายใน (เดี่ยว)",
"internalStatsOpenButton": "เปิดสถิติ (เดี่ยว)",
"internalStatsExportPdf": "ส่งออก PDF",
"internalStatsAgeFilter": "รุ่นอายุ (เดี่ยว)",
"internalStatsAgeNoClass": "ไม่มีคลาส",
"internalStatsAgeSelectAll": "ทั้งหมด",
"internalStatsAgeSelectNone": "ไม่มี",
"internalStatsAgeFilterAll": "ทุกรุ่นอายุ",
"internalStatsAgeFilterNone": "ไม่ได้เลือกรุ่นอายุ",
"internalStatsPeriod": "ช่วงเวลา",
"internalStatsLast12Months": "12 เดือนล่าสุด",
"internalStatsLast6Months": "6 เดือนล่าสุด",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Istatistika ng internal na tournament (singles)",
"internalStatsOpenButton": "Buksan ang istatistika (singles)",
"internalStatsExportPdf": "I-export bilang PDF",
"internalStatsAgeFilter": "Mga pangkat ng edad (singles)",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",
"internalStatsAgeFilterAll": "Lahat ng pangkat ng edad",
"internalStatsAgeFilterNone": "Walang napiling pangkat ng edad",
"internalStatsPeriod": "Saklaw",
"internalStatsLast12Months": "Huling 12 buwan",
"internalStatsLast6Months": "Huling 6 na buwan",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "内部锦标赛统计(单打)",
"internalStatsOpenButton": "打开单打统计数据",
"internalStatsExportPdf": "导出 PDF",
"internalStatsAgeFilter": "年龄组(单打)",
"internalStatsAgeNoClass": "未分配级别",
"internalStatsAgeSelectAll": "全选",
"internalStatsAgeSelectNone": "全不选",
"internalStatsAgeFilterAll": "全部年龄组",
"internalStatsAgeFilterNone": "未选择年龄组",
"internalStatsPeriod": "时间范围",
"internalStatsLast12Months": "过去 12 个月",
"internalStatsLast6Months": "过去 6 个月",