feat(TournamentStats): integrate InternalTournamentStats dialog and state management
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 36s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 36s
- Added InternalTournamentStats component to App.vue for displaying tournament statistics. - Implemented state management for the dialog's visibility using Vuex, allowing for better control of the dialog's open state. - Updated the TournamentsView to utilize the new Vuex mutation for opening the statistics dialog. - Enhanced the InternalTournamentStats component with a new gender selection dropdown, replacing the previous checkbox implementation for improved user experience. - Updated localization strings to support new filtering options and terminology related to gender and age classes across multiple languages.
This commit is contained in:
@@ -180,6 +180,11 @@
|
||||
|
||||
<!-- Dialog Manager -->
|
||||
<DialogManager />
|
||||
|
||||
<InternalTournamentStats
|
||||
v-if="showInternalTournamentStatsDialog"
|
||||
v-model="internalTournamentStatsDialogOpen"
|
||||
/>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
@@ -223,14 +228,17 @@ import BaseDialog from './components/BaseDialog.vue';
|
||||
import { buildInfoConfig, buildConfirmConfig } from './utils/dialogUtils.js';
|
||||
|
||||
const DialogManager = defineAsyncComponent(() => import('./components/DialogManager.vue'));
|
||||
import InternalTournamentStats from './components/tournament/InternalTournamentStats.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
DialogManager
|
||||
,
|
||||
BaseDialog,
|
||||
InfoDialog,
|
||||
ConfirmDialog},
|
||||
DialogManager,
|
||||
InternalTournamentStats,
|
||||
BaseDialog,
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Dialog States
|
||||
@@ -286,7 +294,22 @@ export default {
|
||||
},
|
||||
viewReloadKey() {
|
||||
return `${this.$route.fullPath}|${this.currentClub || 'no-club'}`;
|
||||
}
|
||||
},
|
||||
internalTournamentStatsDialogOpen: {
|
||||
get() {
|
||||
return this.$store.state.internalTournamentStatsOpen;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setInternalTournamentStatsOpen', v);
|
||||
},
|
||||
},
|
||||
showInternalTournamentStatsDialog() {
|
||||
return (
|
||||
this.isAuthenticated &&
|
||||
!!this.currentClub &&
|
||||
this.hasPermission('tournaments', 'read')
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentClub(newVal) {
|
||||
|
||||
@@ -62,17 +62,17 @@
|
||||
</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>
|
||||
<label class="gender-select-label">
|
||||
<select
|
||||
v-model="genderScope"
|
||||
class="gender-scope-select"
|
||||
:disabled="loading"
|
||||
@change="onGenderScopeChange"
|
||||
>
|
||||
<option value="all">{{ $t('tournaments.internalStatsGenderScopeAll') }}</option>
|
||||
<option value="female">{{ $t('tournaments.internalStatsGenderScopeGirls') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -156,8 +156,8 @@ export default {
|
||||
},
|
||||
/** '9'|'11'|…|'19'|'adult' */
|
||||
selectedBandKeys: [],
|
||||
/** 'female'|'open' */
|
||||
selectedGenders: [],
|
||||
/** 'all' = weiblich + offen; 'female' = nur Mädchen/Weiblich-Kanal */
|
||||
genderScope: 'all',
|
||||
ageFilterInitialized: false,
|
||||
pendingResetAgeSelection: false,
|
||||
};
|
||||
@@ -191,18 +191,15 @@ export default {
|
||||
}
|
||||
return [...byKey.values()].sort((a, b) => a.sortKey - b.sortKey);
|
||||
},
|
||||
genderFilterOptions() {
|
||||
return [
|
||||
{ mode: 'female', label: this.tournamentClassGenderLabelFromMode('female') },
|
||||
{ mode: 'open', label: this.tournamentClassGenderLabelFromMode('open') },
|
||||
];
|
||||
effectiveGenderModes() {
|
||||
return this.genderScope === 'female' ? ['female'] : ['female', '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) {
|
||||
for (const g of this.effectiveGenderModes) {
|
||||
const key = bk === 'adult' ? `tt|adult|${g}` : `tt|${bk}|${g}`;
|
||||
if (valid.has(key)) keys.push(key);
|
||||
}
|
||||
@@ -214,7 +211,7 @@ export default {
|
||||
clubId() {
|
||||
this.ageFilterInitialized = false;
|
||||
this.selectedBandKeys = [];
|
||||
this.selectedGenders = [];
|
||||
this.genderScope = 'all';
|
||||
this.pendingResetAgeSelection = false;
|
||||
if (this.modelValue) this.load();
|
||||
},
|
||||
@@ -289,32 +286,19 @@ export default {
|
||||
}
|
||||
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);
|
||||
}
|
||||
onGenderScopeChange() {
|
||||
this.load();
|
||||
},
|
||||
selectAllAgeKeys() {
|
||||
const opts = this.stats.ageClassOptions || [];
|
||||
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
|
||||
this.selectedGenders = ['female', 'open'];
|
||||
this.genderScope = 'all';
|
||||
this.load();
|
||||
},
|
||||
selectNoAgeKeys() {
|
||||
this.selectedBandKeys = [];
|
||||
this.selectedGenders = [];
|
||||
this.load();
|
||||
},
|
||||
/** TT: nur Weiblich vs. Alle (offen) */
|
||||
tournamentClassGenderLabelFromMode(genderMode) {
|
||||
if (genderMode === 'female') return this.$t('tournaments.tournamentClassGenderFemale');
|
||||
return this.$t('tournaments.tournamentClassGenderOpen');
|
||||
},
|
||||
formatBandOnly(b) {
|
||||
if (!b) return '';
|
||||
if (b.band === 'youth' && b.bandNum != null) return `J${b.bandNum}`;
|
||||
@@ -335,14 +319,15 @@ export default {
|
||||
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 genderLabel =
|
||||
this.genderScope === 'female'
|
||||
? this.$t('tournaments.internalStatsGenderScopeGirls')
|
||||
: this.$t('tournaments.internalStatsGenderScopeAll');
|
||||
const parts = [];
|
||||
if (bandLabels.length) {
|
||||
parts.push(`${this.$t('tournaments.internalStatsFilterAgeBands')}: ${bandLabels.join(', ')}`);
|
||||
}
|
||||
if (genderLabels.length) {
|
||||
parts.push(`${this.$t('tournaments.internalStatsFilterGenderColumn')}: ${genderLabels.join(', ')}`);
|
||||
}
|
||||
parts.push(`${this.$t('tournaments.internalStatsFilterGenderColumn')}: ${genderLabel}`);
|
||||
return parts.length ? `${this.$t('tournaments.internalStatsAgeFilter')}: ${parts.join(' · ')}` : '';
|
||||
},
|
||||
exportPdf() {
|
||||
@@ -486,12 +471,12 @@ export default {
|
||||
const opts = this.stats.ageClassOptions || [];
|
||||
if (this.pendingResetAgeSelection) {
|
||||
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
|
||||
this.selectedGenders = ['female', 'open'];
|
||||
this.genderScope = 'all';
|
||||
this.pendingResetAgeSelection = false;
|
||||
this.ageFilterInitialized = true;
|
||||
} else if (!this.ageFilterInitialized && opts.length) {
|
||||
this.selectedBandKeys = this.allBandKeysFromOptions(opts);
|
||||
this.selectedGenders = ['female', 'open'];
|
||||
this.genderScope = 'all';
|
||||
this.ageFilterInitialized = true;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -608,6 +593,22 @@ export default {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.gender-select-label {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gender-scope-select {
|
||||
width: 100%;
|
||||
max-width: 14rem;
|
||||
padding: 0.4rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color, #d1d5db);
|
||||
font-size: 0.88rem;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.age-filter-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -179,6 +179,10 @@
|
||||
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
|
||||
"internalStatsExportPdf": "Als PDF exportieren",
|
||||
"internalStatsAgeFilter": "Altersklassä & Gschlächt (Einzel)",
|
||||
"internalStatsFilterAgeBands": "Altersklassä",
|
||||
"internalStatsFilterGenderColumn": "Gschlächt",
|
||||
"internalStatsGenderScopeAll": "Alli",
|
||||
"internalStatsGenderScopeGirls": "Mädchen",
|
||||
"tournamentClassGenderFemale": "Weiblich",
|
||||
"tournamentClassGenderOpen": "Alli",
|
||||
"internalStatsTtAdult": "Erwachsene",
|
||||
|
||||
@@ -410,6 +410,10 @@
|
||||
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
|
||||
"internalStatsExportPdf": "Als PDF exportieren",
|
||||
"internalStatsAgeFilter": "Altersklassen & Geschlecht (Einzel)",
|
||||
"internalStatsFilterAgeBands": "Altersklassen",
|
||||
"internalStatsFilterGenderColumn": "Geschlecht",
|
||||
"internalStatsGenderScopeAll": "Alle",
|
||||
"internalStatsGenderScopeGirls": "Mädchen",
|
||||
"tournamentClassGenderFemale": "Weiblich",
|
||||
"tournamentClassGenderOpen": "Alle",
|
||||
"internalStatsTtAdult": "Erwachsene",
|
||||
|
||||
@@ -704,6 +704,8 @@
|
||||
"internalStatsAgeFilter": "Altersklassen & Geschlecht (Einzel)",
|
||||
"internalStatsFilterAgeBands": "Altersklassen",
|
||||
"internalStatsFilterGenderColumn": "Geschlecht",
|
||||
"internalStatsGenderScopeAll": "Alle",
|
||||
"internalStatsGenderScopeGirls": "Mädchen",
|
||||
"tournamentClassGenderFemale": "Weiblich",
|
||||
"tournamentClassGenderOpen": "Alle",
|
||||
"internalStatsTtAdult": "Erwachsene",
|
||||
@@ -717,7 +719,7 @@
|
||||
"internalStatsLast6Months": "Letzte 6 Monate",
|
||||
"internalStatsLast3Months": "Letzte 3 Monate",
|
||||
"internalStatsTournamentsInPeriod": "{count} Turnier(e) im Zeitraum (ohne Minimeisterschaften).",
|
||||
"internalStatsPointsExplain": "Wertung: Pro Gruppe wird die Platzierung als Prozentzahl ausgedrückt (bei N Teilnehmern mit Platzierung: 1. = 100 %, Letzter = 0 %, dazwischen linear; gleiche Platzierung = gleicher Wert). N umfasst alle Platzierten in der Gruppe (inkl. Gäste). Bei nur einem Teilnehmer: 100 %. Wer die K.-o.-Runde erreicht, erhält den höchsten Gruppenwert der Klasse plus 1, danach je gewonnenes K.-o.-Spiel einen weiteren Punkt. Nur Vereinsmitglieder (Einzel). Die Filter J9–J19 / Erwachsene und Weiblich/Alle beziehen sich auf das jeweilige Mitglied (Geburtsdatum und Geschlecht laut Vereinsdaten), nicht auf die Bezeichnung der Turnierklasse.",
|
||||
"internalStatsPointsExplain": "Wertung: Pro Gruppe wird die Platzierung als Prozentzahl ausgedrückt (bei N Teilnehmern mit Platzierung: 1. = 100 %, Letzter = 0 %, dazwischen linear; gleiche Platzierung = gleicher Wert). N umfasst alle Platzierten in der Gruppe (inkl. Gäste). Bei nur einem Teilnehmer: 100 %. Wer die K.-o.-Runde erreicht, erhält den höchsten Gruppenwert der Klasse plus 1, danach je gewonnenes K.-o.-Spiel einen weiteren Punkt. Nur Vereinsmitglieder (Einzel). Die Filter J9–J19 / Erwachsene und Geschlecht („Alle“ = Weiblich- und Offen-Kanal, „Mädchen“ = nur Weiblich) beziehen sich auf das jeweilige Mitglied (Geburtsdatum und Geschlecht laut Vereinsdaten), nicht auf die Bezeichnung der Turnierklasse.",
|
||||
"internalStatsAbsoluteRank": "Rangliste Gesamtwertung",
|
||||
"internalStatsAverageRank": "Rangliste Durchschnitt (pro Turnier)",
|
||||
"internalStatsPoints": "Summe",
|
||||
|
||||
@@ -179,6 +179,10 @@
|
||||
"internalStatsOpenButton": "Tournament statistics (singles)",
|
||||
"internalStatsExportPdf": "Export as PDF",
|
||||
"internalStatsAgeFilter": "Age group & gender (singles)",
|
||||
"internalStatsFilterAgeBands": "Age classes",
|
||||
"internalStatsFilterGenderColumn": "Gender",
|
||||
"internalStatsGenderScopeAll": "All",
|
||||
"internalStatsGenderScopeGirls": "Girls only",
|
||||
"tournamentClassGenderFemale": "Female",
|
||||
"tournamentClassGenderOpen": "Open (all)",
|
||||
"internalStatsTtAdult": "Adults",
|
||||
|
||||
@@ -362,6 +362,8 @@
|
||||
"internalStatsAgeFilter": "Age group & gender (singles)",
|
||||
"internalStatsFilterAgeBands": "Age classes",
|
||||
"internalStatsFilterGenderColumn": "Gender",
|
||||
"internalStatsGenderScopeAll": "All",
|
||||
"internalStatsGenderScopeGirls": "Girls only",
|
||||
"tournamentClassGenderFemale": "Female",
|
||||
"tournamentClassGenderOpen": "Open (all)",
|
||||
"internalStatsTtAdult": "Adults",
|
||||
@@ -375,7 +377,7 @@
|
||||
"internalStatsLast6Months": "Last 6 months",
|
||||
"internalStatsLast3Months": "Last 3 months",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournament(s) in this period (excluding mini championships).",
|
||||
"internalStatsPointsExplain": "Scoring: In each group, placement is expressed as a percentage (with N ranked players: 1st = 100%, last = 0%, linear in between; tied ranks share the same value). N counts everyone ranked in that group (including guests). With only one player: 100%. Players who reach the knockout get the highest group score in that class plus 1, then one extra point per knockout match won. Club members in singles classes only. The J9–J19 / adults and female/open filters use each member’s birth date and gender from club records, not the tournament class name.",
|
||||
"internalStatsPointsExplain": "Scoring: In each group, placement is expressed as a percentage (with N ranked players: 1st = 100%, last = 0%, linear in between; tied ranks share the same value). N counts everyone ranked in that group (including guests). With only one player: 100%. Players who reach the knockout get the highest group score in that class plus 1, then one extra point per knockout match won. Club members in singles classes only. Age bands (J9–J19 / adults) and gender (“All” = female and open channels, “Girls only” = female channel) use each member’s birth date and gender from club records, not the tournament class name.",
|
||||
"internalStatsAbsoluteRank": "Total score ranking",
|
||||
"internalStatsAverageRank": "Average per tournament",
|
||||
"internalStatsPoints": "Total",
|
||||
|
||||
@@ -179,6 +179,10 @@
|
||||
"internalStatsOpenButton": "Tournament statistics (singles)",
|
||||
"internalStatsExportPdf": "Export as PDF",
|
||||
"internalStatsAgeFilter": "Age group & gender (singles)",
|
||||
"internalStatsFilterAgeBands": "Age classes",
|
||||
"internalStatsFilterGenderColumn": "Gender",
|
||||
"internalStatsGenderScopeAll": "All",
|
||||
"internalStatsGenderScopeGirls": "Girls only",
|
||||
"tournamentClassGenderFemale": "Female",
|
||||
"tournamentClassGenderOpen": "Open (all)",
|
||||
"internalStatsTtAdult": "Adults",
|
||||
|
||||
@@ -43,6 +43,8 @@ const store = createStore({
|
||||
// Browser-Sprache wird in i18n/index.js erkannt
|
||||
return null;
|
||||
})(),
|
||||
/** Turnierstatistik-Einzel: Dialog bleibt beim Seitenwechsel offen (App.vue) */
|
||||
internalTournamentStatsOpen: false,
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
@@ -72,6 +74,7 @@ const store = createStore({
|
||||
safeSessionStorage.setItem('currentClub', club);
|
||||
} else {
|
||||
safeSessionStorage.removeItem('currentClub');
|
||||
state.internalTournamentStatsOpen = false;
|
||||
}
|
||||
},
|
||||
setClubsMutation(state, clubs) {
|
||||
@@ -100,7 +103,8 @@ const store = createStore({
|
||||
clearToken(state) {
|
||||
state.token = null;
|
||||
safeSessionStorage.removeItem('token');
|
||||
safeSessionStorage.removeItem('currentClub');
|
||||
safeSessionStorage.removeItem('currentClub');
|
||||
state.internalTournamentStatsOpen = false;
|
||||
},
|
||||
clearUsername(state) {
|
||||
state.username = '';
|
||||
@@ -142,7 +146,10 @@ const store = createStore({
|
||||
const maxZIndex = Math.max(...state.dialogs.map(d => d.zIndex));
|
||||
dialog.zIndex = maxZIndex + 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
setInternalTournamentStatsOpen(state, open) {
|
||||
state.internalTournamentStatsOpen = !!open;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async login({ commit }, { token, username }) {
|
||||
@@ -153,6 +160,7 @@ const store = createStore({
|
||||
commit('setClubsMutation', response.data);
|
||||
},
|
||||
logout({ commit }) {
|
||||
commit('setInternalTournamentStatsOpen', false);
|
||||
commit('clearToken');
|
||||
commit('clearUsername');
|
||||
commit('clearPermissions');
|
||||
|
||||
@@ -34,14 +34,12 @@
|
||||
v-if="activeMode === 'internal'"
|
||||
type="button"
|
||||
class="stats-open-button"
|
||||
@click="internalStatsOpen = true"
|
||||
@click="$store.commit('setInternalTournamentStatsOpen', true)"
|
||||
>
|
||||
📊 {{ $t('tournaments.internalStatsOpenButton') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<InternalTournamentStats v-if="activeMode === 'internal'" v-model="internalStatsOpen" />
|
||||
|
||||
<div class="tab-content">
|
||||
<TournamentTab
|
||||
:key="activeMode"
|
||||
@@ -54,18 +52,15 @@
|
||||
|
||||
<script>
|
||||
import TournamentTab from './TournamentTab.vue';
|
||||
import InternalTournamentStats from '../components/tournament/InternalTournamentStats.vue';
|
||||
|
||||
export default {
|
||||
name: 'TournamentsView',
|
||||
components: {
|
||||
TournamentTab,
|
||||
InternalTournamentStats,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeMode: 'internal',
|
||||
internalStatsOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
Reference in New Issue
Block a user