feat(TournamentStats): update age class filtering UI and logic in InternalTournamentStats component
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s

- Replaced age class filtering with band and gender options for improved user selection.
- Introduced new methods for handling band and gender checkbox changes, enhancing the filtering logic.
- Updated the component's state management to accommodate selected bands and genders.
- Enhanced localization strings to support new filtering options, improving user accessibility and understanding.
This commit is contained in:
Torsten Schulz (local)
2026-04-08 14:01:47 +02:00
parent bbd9f08e97
commit 003b8fd3bc
3 changed files with 161 additions and 63 deletions

View File

@@ -35,7 +35,7 @@
</p>
<p class="stats-explain">{{ $t('tournaments.internalStatsPointsExplain') }}</p>
<fieldset v-if="sortedAgeOptions.length" class="age-filter">
<fieldset v-if="bandOptions.length" 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">
@@ -45,16 +45,35 @@
{{ $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 class="age-filter-columns">
<div class="age-filter-column">
<div class="age-filter-column-title">{{ $t('tournaments.internalStatsFilterAgeBands') }}</div>
<div class="age-filter-checkboxes">
<label v-for="b in bandOptions" :key="'b-' + b.bandKey" class="age-filter-item">
<input
type="checkbox"
:checked="selectedBandKeys.includes(b.bandKey)"
:disabled="loading"
@change="onBandCheckboxChange(b.bandKey, $event.target.checked)"
/>
<span>{{ formatBandOnly(b) }}</span>
</label>
</div>
</div>
<div class="age-filter-column">
<div class="age-filter-column-title">{{ $t('tournaments.internalStatsFilterGenderColumn') }}</div>
<div class="age-filter-checkboxes">
<label v-for="g in genderFilterOptions" :key="'g-' + g.mode" class="age-filter-item">
<input
type="checkbox"
:checked="selectedGenders.includes(g.mode)"
:disabled="loading"
@change="onGenderCheckboxChange(g.mode, $event.target.checked)"
/>
<span>{{ g.label }}</span>
</label>
</div>
</div>
</div>
</fieldset>
@@ -135,7 +154,10 @@ export default {
absoluteRanking: [],
averageRanking: [],
},
selectedAgeKeys: [],
/** '9'|'11'|…|'19'|'adult' */
selectedBandKeys: [],
/** 'female'|'open' */
selectedGenders: [],
ageFilterInitialized: false,
pendingResetAgeSelection: false,
};
@@ -151,21 +173,48 @@ export default {
(!this.stats.absoluteRanking?.length && !this.stats.averageRanking?.length)
);
},
sortedAgeOptions() {
const list = [...(this.stats.ageClassOptions || [])];
list.sort((a, b) => {
const va = this.ttOptionSortKey(a);
const vb = this.ttOptionSortKey(b);
if (va !== vb) return va - vb;
return (a.key || '').localeCompare(b.key || '', 'de');
});
return list;
/** Eine Zeile pro Altersband (ohne Geschlecht), sortiert J9 … Erwachsene */
bandOptions() {
const opts = this.stats.ageClassOptions || [];
const byKey = new Map();
for (const o of opts) {
if (!o || o.isNoClass) continue;
const bandKey = o.band === 'adult' ? 'adult' : String(o.bandNum);
if (!byKey.has(bandKey)) {
byKey.set(bandKey, {
bandKey,
band: o.band,
bandNum: o.bandNum,
sortKey: o.band === 'adult' ? 1000 : Number(o.bandNum) || 0,
});
}
}
return [...byKey.values()].sort((a, b) => a.sortKey - b.sortKey);
},
genderFilterOptions() {
return [
{ mode: 'female', label: this.tournamentClassGenderLabelFromMode('female') },
{ mode: 'open', label: this.tournamentClassGenderLabelFromMode('open') },
];
},
effectiveAgeClassKeys() {
const opts = this.stats.ageClassOptions || [];
const valid = new Set(opts.map((o) => o.key));
const keys = [];
for (const bk of this.selectedBandKeys) {
for (const g of this.selectedGenders) {
const key = bk === 'adult' ? `tt|adult|${g}` : `tt|${bk}|${g}`;
if (valid.has(key)) keys.push(key);
}
}
return keys;
},
},
watch: {
clubId() {
this.ageFilterInitialized = false;
this.selectedAgeKeys = [];
this.selectedBandKeys = [];
this.selectedGenders = [];
this.pendingResetAgeSelection = false;
if (this.modelValue) this.load();
},
@@ -200,10 +249,11 @@ export default {
const opts = this.stats.ageClassOptions || [];
const allKeys = opts.map((o) => o.key);
if (allKeys.length === 0) return params;
if (this.selectedAgeKeys.length === 0) {
const sel = this.effectiveAgeClassKeys;
if (sel.length === 0) {
params.ageClassKeys = '';
} else if (this.selectedAgeKeys.length < allKeys.length) {
params.ageClassKeys = this.selectedAgeKeys.join(',');
} else if (sel.length < allKeys.length) {
params.ageClassKeys = sel.join(',');
}
return params;
},
@@ -211,22 +261,53 @@ export default {
this.pendingResetAgeSelection = true;
this.load();
},
onAgeCheckboxChange(key, checked) {
allBandKeysFromOptions(opts) {
const seen = new Set();
const out = [];
for (const o of opts || []) {
if (!o || o.isNoClass) continue;
const bk = o.band === 'adult' ? 'adult' : String(o.bandNum);
if (!seen.has(bk)) {
seen.add(bk);
out.push(bk);
}
}
out.sort((a, b) => {
if (a === 'adult') return 1;
if (b === 'adult') return -1;
return Number(a) - Number(b);
});
return out;
},
onBandCheckboxChange(bandKey, checked) {
if (checked) {
if (!this.selectedAgeKeys.includes(key)) {
this.selectedAgeKeys = [...this.selectedAgeKeys, key];
if (!this.selectedBandKeys.includes(bandKey)) {
this.selectedBandKeys = [...this.selectedBandKeys, bandKey];
}
} else {
this.selectedAgeKeys = this.selectedAgeKeys.filter((k) => k !== key);
this.selectedBandKeys = this.selectedBandKeys.filter((k) => k !== bandKey);
}
this.load();
},
onGenderCheckboxChange(mode, checked) {
if (checked) {
if (!this.selectedGenders.includes(mode)) {
this.selectedGenders = [...this.selectedGenders, mode];
}
} else {
this.selectedGenders = this.selectedGenders.filter((m) => m !== mode);
}
this.load();
},
selectAllAgeKeys() {
this.selectedAgeKeys = (this.stats.ageClassOptions || []).map((o) => o.key);
const opts = this.stats.ageClassOptions || [];
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
this.selectedGenders = ['female', 'open'];
this.load();
},
selectNoAgeKeys() {
this.selectedAgeKeys = [];
this.selectedBandKeys = [];
this.selectedGenders = [];
this.load();
},
/** TT: nur Weiblich vs. Alle (offen) */
@@ -234,44 +315,35 @@ export default {
if (genderMode === 'female') return this.$t('tournaments.tournamentClassGenderFemale');
return this.$t('tournaments.tournamentClassGenderOpen');
},
/** Sortierung: J9w, J9o, J11w … Erwachsene, zuletzt ohne Zuordnung */
ttOptionSortKey(opt) {
if (!opt) return 0;
if (opt.isNoClass) return 1e6;
if (opt.band === 'adult') return 5000 + (opt.genderMode === 'female' ? 0 : 1);
if (opt.band === 'youth' && opt.bandNum != null) {
return opt.bandNum * 10 + (opt.genderMode === 'female' ? 0 : 1);
}
return 9999;
},
formatAgeOption(opt) {
if (!opt) return '';
if (opt.isNoClass) return this.$t('tournaments.internalStatsAgeNoClass');
if (opt.band === 'youth' && opt.bandNum != null) {
return `J${opt.bandNum} · ${this.tournamentClassGenderLabelFromMode(opt.genderMode)}`;
}
if (opt.band === 'adult') {
return `${this.$t('tournaments.internalStatsTtAdult')} · ${this.tournamentClassGenderLabelFromMode(opt.genderMode)}`;
}
return opt.key || '';
formatBandOnly(b) {
if (!b) return '';
if (b.band === 'youth' && b.bandNum != null) return `J${b.bandNum}`;
if (b.band === 'adult') return this.$t('tournaments.internalStatsTtAdult');
return b.bandKey || '';
},
ageFilterPdfLine() {
const opts = this.stats.ageClassOptions || [];
if (opts.length === 0) return '';
const allKeys = opts.map((o) => o.key);
if (this.selectedAgeKeys.length === allKeys.length) {
const sel = this.effectiveAgeClassKeys;
if (sel.length === allKeys.length) {
return this.$t('tournaments.internalStatsAgeFilterAll');
}
if (this.selectedAgeKeys.length === 0) {
if (sel.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}`;
const bandLabels = this.bandOptions
.filter((b) => this.selectedBandKeys.includes(b.bandKey))
.map((b) => this.formatBandOnly(b));
const genderLabels = this.selectedGenders.map((m) => this.tournamentClassGenderLabelFromMode(m));
const parts = [];
if (bandLabels.length) {
parts.push(`${this.$t('tournaments.internalStatsFilterAgeBands')}: ${bandLabels.join(', ')}`);
}
if (genderLabels.length) {
parts.push(`${this.$t('tournaments.internalStatsFilterGenderColumn')}: ${genderLabels.join(', ')}`);
}
return parts.length ? `${this.$t('tournaments.internalStatsAgeFilter')}: ${parts.join(' · ')}` : '';
},
exportPdf() {
const t = this.$t;
@@ -413,11 +485,13 @@ export default {
this.stats = res.data || {};
const opts = this.stats.ageClassOptions || [];
if (this.pendingResetAgeSelection) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
this.selectedGenders = ['female', 'open'];
this.pendingResetAgeSelection = false;
this.ageFilterInitialized = true;
} else if (!this.ageFilterInitialized && opts.length) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
this.selectedGenders = ['female', 'open'];
this.ageFilterInitialized = true;
}
} catch (e) {
@@ -514,6 +588,26 @@ export default {
color: #374151;
}
.age-filter-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem 1.5rem;
align-items: start;
}
@media (max-width: 520px) {
.age-filter-columns {
grid-template-columns: 1fr;
}
}
.age-filter-column-title {
font-size: 0.82rem;
font-weight: 600;
color: #6b7280;
margin-bottom: 0.4rem;
}
.age-filter-actions {
display: flex;
gap: 0.75rem;
@@ -544,7 +638,7 @@ export default {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 11rem;
max-height: 14rem;
overflow-y: auto;
}

View File

@@ -702,6 +702,8 @@
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen & Geschlecht (Einzel)",
"internalStatsFilterAgeBands": "Altersklassen",
"internalStatsFilterGenderColumn": "Geschlecht",
"tournamentClassGenderFemale": "Weiblich",
"tournamentClassGenderOpen": "Alle",
"internalStatsTtAdult": "Erwachsene",

View File

@@ -360,6 +360,8 @@
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age group & gender (singles)",
"internalStatsFilterAgeBands": "Age classes",
"internalStatsFilterGenderColumn": "Gender",
"tournamentClassGenderFemale": "Female",
"tournamentClassGenderOpen": "Open (all)",
"internalStatsTtAdult": "Adults",