feat(TrainingStats): enhance training statistics view with collapsible panels and localization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
- Refactored the TrainingStatsView to implement collapsible sections for better organization of training statistics. - Added new localization keys for training statistics panels in both German and English. - Updated the mobile app's TrainingStatsScreen to utilize the new collapsible panel structure, improving user experience. - Enhanced the MembersManager to merge training statistics into member data, providing a comprehensive view of member participation. - Introduced new API methods for quick updates and transfers of member data, streamlining member management processes.
This commit is contained in:
@@ -1353,7 +1353,14 @@
|
||||
"participationsTotal": "Teilnahmen (Gesamt)",
|
||||
"lastTraining": "Letztes Training",
|
||||
"actions": "Aktionen",
|
||||
"showDetails": "Details anzeigen"
|
||||
"showDetails": "Details anzeigen",
|
||||
"panelSummary": "Kennzahlen (Filter)",
|
||||
"panelMonthlyTrend": "Monatlicher Verlauf",
|
||||
"panelWeekdayStats": "Trainingstage nach Wochentag",
|
||||
"panelMemberStructure": "Mitgliederstruktur",
|
||||
"panelBestDay": "Stärkster Trainingstag",
|
||||
"panelGroupPerformance": "Entwicklung pro Gruppe",
|
||||
"panelAgeGroups": "Anwesenheit nach Altersklasse"
|
||||
},
|
||||
"tournament": {
|
||||
"apply": "Übernehmen"
|
||||
|
||||
@@ -975,7 +975,14 @@
|
||||
"participationsTotal": "Participations (total)",
|
||||
"lastTraining": "Last training",
|
||||
"actions": "Actions",
|
||||
"showDetails": "Show details"
|
||||
"showDetails": "Show details",
|
||||
"panelSummary": "Key figures (filtered)",
|
||||
"panelMonthlyTrend": "Monthly trend",
|
||||
"panelWeekdayStats": "Training days by weekday",
|
||||
"panelMemberStructure": "Member structure",
|
||||
"panelBestDay": "Busiest training day",
|
||||
"panelGroupPerformance": "Progress by group",
|
||||
"panelAgeGroups": "Attendance by age class"
|
||||
},
|
||||
"courtDrawingTool": {
|
||||
"title": "Table tennis exercise drawing",
|
||||
|
||||
@@ -26,153 +26,194 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="stats-overview">
|
||||
<div class="stats-summary">
|
||||
<div class="stat-card">
|
||||
<h3>Aktive Mitglieder</h3>
|
||||
<div class="stat-number">{{ filteredMembers.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Trainingstage 12 Monate</h3>
|
||||
<div class="stat-number">{{ filteredTrainingDays.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Ø Teilnehmer je Training</h3>
|
||||
<div class="stat-number">{{ filteredOverview.averageParticipants.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Teilnahmen gesamt</h3>
|
||||
<div class="stat-number">{{ filteredOverview.totalParticipants }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Anwesenheitsquote 12 Monate</h3>
|
||||
<div class="stat-number">{{ filteredOverview.attendanceRate.toFixed(1) }}%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Nicht im Training</h3>
|
||||
<div class="stat-number">{{ filteredMembers.filter((member) => member.notInTraining).length }}</div>
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="togglePanel('overview')">
|
||||
<h3>{{ $t('trainingStats.panelSummary') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="toggle-icon">{{ panels.overview ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="panels.overview" class="section-content">
|
||||
<div class="stats-overview panel-body-padding">
|
||||
<div class="stats-summary">
|
||||
<div class="stat-card">
|
||||
<h3>Aktive Mitglieder</h3>
|
||||
<div class="stat-number">{{ filteredMembers.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Trainingstage 12 Monate</h3>
|
||||
<div class="stat-number">{{ filteredTrainingDays.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Ø Teilnehmer je Training</h3>
|
||||
<div class="stat-number">{{ filteredOverview.averageParticipants.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Teilnahmen gesamt</h3>
|
||||
<div class="stat-number">{{ filteredOverview.totalParticipants }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Anwesenheitsquote 12 Monate</h3>
|
||||
<div class="stat-number">{{ filteredOverview.attendanceRate.toFixed(1) }}%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Nicht im Training</h3>
|
||||
<div class="stat-number">{{ filteredMembers.filter((member) => member.notInTraining).length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-panels-grid">
|
||||
<section class="stats-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Monatlicher Verlauf</h3>
|
||||
<span>{{ filteredMonthlyTrend.length }} Monate</span>
|
||||
<div class="collapsible-section stats-panel-collapsible">
|
||||
<div class="section-header" @click="togglePanel('monthlyTrend')">
|
||||
<h3>{{ $t('trainingStats.panelMonthlyTrend') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="section-meta">{{ filteredMonthlyTrend.length }} Monate</span>
|
||||
<span class="toggle-icon">{{ panels.monthlyTrend ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="trend-list">
|
||||
<div v-for="month in filteredMonthlyTrend" :key="month.key" class="trend-row">
|
||||
<div class="trend-meta">
|
||||
<strong>{{ month.label }}</strong>
|
||||
<span>{{ month.trainingCount }} Trainingstage</span>
|
||||
</div>
|
||||
<div class="trend-bar-track">
|
||||
<div class="trend-bar-fill" :style="{ width: `${getMonthBarWidth(month.averageParticipants)}%` }"></div>
|
||||
</div>
|
||||
<div class="trend-value">{{ month.averageParticipants.toFixed(1) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Trainingstage nach Wochentag</h3>
|
||||
<span>{{ filteredWeekdayStats.length }} Wochentage</span>
|
||||
</div>
|
||||
<div class="weekday-grid">
|
||||
<div v-for="day in filteredWeekdayStats" :key="day.weekday" class="weekday-card">
|
||||
<strong>{{ day.weekday }}</strong>
|
||||
<span>{{ day.trainingCount }} Termine</span>
|
||||
<span>Ø {{ day.averageParticipants.toFixed(1) }} Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Mitgliederstruktur</h3>
|
||||
</div>
|
||||
<div class="distribution-grid">
|
||||
<div class="distribution-card">
|
||||
<strong>Sehr aktiv</strong>
|
||||
<span>{{ filteredMemberDistribution.highlyActive }}</span>
|
||||
<small>mind. 75 % der Trainingstage</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Regelmäßig</strong>
|
||||
<span>{{ filteredMemberDistribution.regular }}</span>
|
||||
<small>40 bis unter 75 %</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Gelegentlich</strong>
|
||||
<span>{{ filteredMemberDistribution.occasional }}</span>
|
||||
<small>unter 40 %</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Ohne Teilnahme</strong>
|
||||
<span>{{ filteredMemberDistribution.inactive }}</span>
|
||||
<small>0 Teilnahmen in 12 Monaten</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-panel stats-panel-highlight">
|
||||
<div class="panel-header">
|
||||
<h3>Stärkster Trainingstag</h3>
|
||||
</div>
|
||||
<div v-if="filteredOverview.bestTrainingDay" class="highlight-block">
|
||||
<strong>{{ formatDate(filteredOverview.bestTrainingDay.date) }}</strong>
|
||||
<span>{{ getWeekday(filteredOverview.bestTrainingDay.date) }}</span>
|
||||
<div class="highlight-number">{{ filteredOverview.bestTrainingDay.participantCount }}</div>
|
||||
<small>Teilnehmer beim bestbesuchten Training der letzten 12 Monate</small>
|
||||
</div>
|
||||
<div v-else class="highlight-block">
|
||||
<strong>Keine Daten</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Entwicklung pro Gruppe</h3>
|
||||
<span>{{ groupPerformance.length }} Gruppen</span>
|
||||
</div>
|
||||
<div class="group-performance-list">
|
||||
<div v-for="group in groupPerformance" :key="group.name" class="group-performance-row">
|
||||
<div class="group-performance-meta">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<span>{{ group.memberCount }} Mitglieder</span>
|
||||
</div>
|
||||
<div class="group-performance-stats">
|
||||
<span>{{ group.averageParticipations12Months.toFixed(1) }} Ø Teilnahmen / 12 Monate</span>
|
||||
<span>{{ group.participationRate.toFixed(1) }}% Anwesenheit</span>
|
||||
<div v-show="panels.monthlyTrend" class="section-content">
|
||||
<div class="trend-list panel-body-padding">
|
||||
<div v-for="month in filteredMonthlyTrend" :key="month.key" class="trend-row">
|
||||
<div class="trend-meta">
|
||||
<strong>{{ month.label }}</strong>
|
||||
<span>{{ month.trainingCount }} Trainingstage</span>
|
||||
</div>
|
||||
<div class="trend-bar-track">
|
||||
<div class="trend-bar-fill" :style="{ width: `${getMonthBarWidth(month.averageParticipants)}%` }"></div>
|
||||
</div>
|
||||
<div class="trend-value">{{ month.averageParticipants.toFixed(1) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="stats-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Anwesenheit nach Altersklasse</h3>
|
||||
<span>{{ ageGroupStats.length }} Klassen</span>
|
||||
<div class="collapsible-section stats-panel-collapsible">
|
||||
<div class="section-header" @click="togglePanel('weekdayStats')">
|
||||
<h3>{{ $t('trainingStats.panelWeekdayStats') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="section-meta">{{ filteredWeekdayStats.length }} Wochentage</span>
|
||||
<span class="toggle-icon">{{ panels.weekdayStats ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="distribution-grid">
|
||||
<div v-for="entry in ageGroupStats" :key="entry.label" class="distribution-card">
|
||||
<strong>{{ entry.label }}</strong>
|
||||
<span>{{ entry.memberCount }}</span>
|
||||
<small>{{ entry.averageParticipations12Months.toFixed(1) }} Ø Teilnahmen / 12 Monate</small>
|
||||
<div v-show="panels.weekdayStats" class="section-content">
|
||||
<div class="weekday-grid panel-body-padding">
|
||||
<div v-for="day in filteredWeekdayStats" :key="day.weekday" class="weekday-card">
|
||||
<strong>{{ day.weekday }}</strong>
|
||||
<span>{{ day.trainingCount }} Termine</span>
|
||||
<span>Ø {{ day.averageParticipants.toFixed(1) }} Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-section stats-panel-collapsible">
|
||||
<div class="section-header" @click="togglePanel('memberStructure')">
|
||||
<h3>{{ $t('trainingStats.panelMemberStructure') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="toggle-icon">{{ panels.memberStructure ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="panels.memberStructure" class="section-content">
|
||||
<div class="distribution-grid panel-body-padding">
|
||||
<div class="distribution-card">
|
||||
<strong>Sehr aktiv</strong>
|
||||
<span>{{ filteredMemberDistribution.highlyActive }}</span>
|
||||
<small>mind. 75 % der Trainingstage</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Regelmäßig</strong>
|
||||
<span>{{ filteredMemberDistribution.regular }}</span>
|
||||
<small>40 bis unter 75 %</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Gelegentlich</strong>
|
||||
<span>{{ filteredMemberDistribution.occasional }}</span>
|
||||
<small>unter 40 %</small>
|
||||
</div>
|
||||
<div class="distribution-card">
|
||||
<strong>Ohne Teilnahme</strong>
|
||||
<span>{{ filteredMemberDistribution.inactive }}</span>
|
||||
<small>0 Teilnahmen in 12 Monaten</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-section stats-panel-collapsible stats-panel-highlight">
|
||||
<div class="section-header" @click="togglePanel('bestDay')">
|
||||
<h3>{{ $t('trainingStats.panelBestDay') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="toggle-icon">{{ panels.bestDay ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="panels.bestDay" class="section-content">
|
||||
<div class="panel-body-padding">
|
||||
<div v-if="filteredOverview.bestTrainingDay" class="highlight-block">
|
||||
<strong>{{ formatDate(filteredOverview.bestTrainingDay.date) }}</strong>
|
||||
<span>{{ getWeekday(filteredOverview.bestTrainingDay.date) }}</span>
|
||||
<div class="highlight-number">{{ filteredOverview.bestTrainingDay.participantCount }}</div>
|
||||
<small>Teilnehmer beim bestbesuchten Training der letzten 12 Monate</small>
|
||||
</div>
|
||||
<div v-else class="highlight-block">
|
||||
<strong>Keine Daten</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-section stats-panel-collapsible">
|
||||
<div class="section-header" @click="togglePanel('groupPerformance')">
|
||||
<h3>{{ $t('trainingStats.panelGroupPerformance') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="section-meta">{{ groupPerformance.length }} Gruppen</span>
|
||||
<span class="toggle-icon">{{ panels.groupPerformance ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="panels.groupPerformance" class="section-content">
|
||||
<div class="group-performance-list panel-body-padding">
|
||||
<div v-for="group in groupPerformance" :key="group.name" class="group-performance-row">
|
||||
<div class="group-performance-meta">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<span>{{ group.memberCount }} Mitglieder</span>
|
||||
</div>
|
||||
<div class="group-performance-stats">
|
||||
<span>{{ group.averageParticipations12Months.toFixed(1) }} Ø Teilnahmen / 12 Monate</span>
|
||||
<span>{{ group.participationRate.toFixed(1) }}% Anwesenheit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-section stats-panel-collapsible">
|
||||
<div class="section-header" @click="togglePanel('ageGroup')">
|
||||
<h3>{{ $t('trainingStats.panelAgeGroups') }}</h3>
|
||||
<span class="section-header-right">
|
||||
<span class="section-meta">{{ ageGroupStats.length }} Klassen</span>
|
||||
<span class="toggle-icon">{{ panels.ageGroup ? '▼' : '▶' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="panels.ageGroup" class="section-content">
|
||||
<div class="distribution-grid panel-body-padding">
|
||||
<div v-for="entry in ageGroupStats" :key="entry.label" class="distribution-card">
|
||||
<strong>{{ entry.label }}</strong>
|
||||
<span>{{ entry.memberCount }}</span>
|
||||
<small>{{ entry.averageParticipations12Months.toFixed(1) }} Ø Teilnahmen / 12 Monate</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trainingstage-Tabelle (standardmäßig aufgeklappt) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleTrainingDays">
|
||||
<div class="section-header" @click="togglePanel('trainingDays')">
|
||||
<h3>{{ $t('trainingStats.trainingDays') }}</h3>
|
||||
<span class="toggle-icon">{{ showTrainingDays ? '▼' : '▶' }}</span>
|
||||
<span class="toggle-icon">{{ panels.trainingDays ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showTrainingDays" class="section-content">
|
||||
<div v-show="panels.trainingDays" class="section-content">
|
||||
<div class="training-days-container">
|
||||
<table class="training-days-table">
|
||||
<thead>
|
||||
@@ -207,13 +248,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mitglieder-Tabelle (standardmäßig eingeklappt) -->
|
||||
<!-- Mitglieder-Tabelle -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleMembers">
|
||||
<div class="section-header" @click="togglePanel('members')">
|
||||
<h3>{{ $t('trainingStats.memberParticipations') }}</h3>
|
||||
<span class="toggle-icon">{{ showMembers ? '▼' : '▶' }}</span>
|
||||
<span class="toggle-icon">{{ panels.members ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showMembers" class="section-content">
|
||||
<div v-show="panels.members" class="section-content">
|
||||
<div class="members-table-container">
|
||||
<table class="members-table">
|
||||
<thead>
|
||||
@@ -576,8 +617,17 @@ export default {
|
||||
loading: false,
|
||||
sortField: 'name',
|
||||
sortDirection: 'asc',
|
||||
showTrainingDays: true,
|
||||
showMembers: false
|
||||
panels: {
|
||||
overview: false,
|
||||
monthlyTrend: false,
|
||||
weekdayStats: false,
|
||||
memberStructure: false,
|
||||
bestDay: false,
|
||||
groupPerformance: false,
|
||||
ageGroup: false,
|
||||
trainingDays: false,
|
||||
members: false
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -626,15 +676,12 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
toggleTrainingDays() {
|
||||
this.showTrainingDays = !this.showTrainingDays;
|
||||
|
||||
togglePanel(key) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.panels, key)) return;
|
||||
this.panels = { ...this.panels, [key]: !this.panels[key] };
|
||||
},
|
||||
|
||||
toggleMembers() {
|
||||
this.showMembers = !this.showMembers;
|
||||
},
|
||||
|
||||
|
||||
getWeekday(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
@@ -779,6 +826,42 @@ export default {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-panels-grid > .collapsible-section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-panel-collapsible .section-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section-meta {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.section-header:hover .section-meta {
|
||||
color: inherit;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.panel-body-padding {
|
||||
padding: 0 1.15rem 1.15rem;
|
||||
}
|
||||
|
||||
.stats-overview.panel-body-padding {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.collapsible-section.stats-panel-highlight {
|
||||
background: linear-gradient(135deg, rgba(47, 122, 95, 0.08), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
Reference in New Issue
Block a user