feat(TournamentStats): update age class filtering UI and logic in InternalTournamentStats component
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user