Änderungen: - Neue Methode `getUserStatistics` im `AdminController` hinzugefügt, um Benutzerstatistiken abzurufen. - Implementierung der Logik zur Berechnung der Gesamtanzahl aktiver Benutzer, Geschlechterverteilung und Altersverteilung im `AdminService`. - Neue Route `/users/statistics` im `adminRouter` definiert, um auf die Benutzerstatistiken zuzugreifen. - Anpassungen der Navigationsstruktur und Übersetzungen für Benutzerstatistiken in den Sprachdateien aktualisiert. Diese Anpassungen verbessern die Analyse der Benutzerbasis und erweitern die Funktionalität des Admin-Bereichs.
219 lines
5.6 KiB
Vue
219 lines
5.6 KiB
Vue
<template>
|
|
<div class="user-statistics-view">
|
|
<h2>{{ $t('admin.userStatistics.title') }}</h2>
|
|
|
|
<div v-if="loading" class="loading">
|
|
{{ $t('general.loading') }}...
|
|
</div>
|
|
|
|
<div v-else-if="error" class="error">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div v-else class="statistics-container">
|
|
<!-- Gesamtstatistik -->
|
|
<div class="stat-card total-users">
|
|
<h3>{{ $t('admin.userStatistics.totalUsers') }}</h3>
|
|
<div class="stat-number">{{ statistics.totalUsers }}</div>
|
|
</div>
|
|
|
|
<!-- Geschlechterverteilung -->
|
|
<div class="stat-card gender-distribution">
|
|
<h3>{{ $t('admin.userStatistics.genderDistribution') }}</h3>
|
|
<div class="gender-chart">
|
|
<div v-for="(count, gender) in statistics.genderDistribution" :key="gender" class="gender-item">
|
|
<span class="gender-label">{{ $t(`gender.${gender}`) }}</span>
|
|
<div class="gender-bar">
|
|
<div class="gender-fill" :style="{ width: getGenderPercentage(count) + '%' }"></div>
|
|
</div>
|
|
<span class="gender-count">{{ count }} ({{ getGenderPercentage(count) }}%)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Altersverteilung -->
|
|
<div class="stat-card age-distribution">
|
|
<h3>{{ $t('admin.userStatistics.ageDistribution') }}</h3>
|
|
<div class="age-chart">
|
|
<div v-for="(count, ageGroup) in statistics.ageGroups" :key="ageGroup" class="age-item">
|
|
<span class="age-label">{{ ageGroup }}</span>
|
|
<div class="age-bar">
|
|
<div class="age-fill" :style="{ width: getAgePercentage(count) + '%' }"></div>
|
|
</div>
|
|
<span class="age-count">{{ count }} ({{ getAgePercentage(count) }}%)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import apiClient from '@/utils/axios.js';
|
|
|
|
export default {
|
|
name: 'UserStatisticsView',
|
|
data() {
|
|
return {
|
|
statistics: {
|
|
totalUsers: 0,
|
|
genderDistribution: {},
|
|
ageGroups: {}
|
|
},
|
|
loading: true,
|
|
error: null
|
|
};
|
|
},
|
|
async mounted() {
|
|
await this.loadStatistics();
|
|
},
|
|
methods: {
|
|
async loadStatistics() {
|
|
try {
|
|
this.loading = true;
|
|
this.error = null;
|
|
const response = await apiClient.get('/api/admin/users/statistics');
|
|
this.statistics = response.data;
|
|
} catch (err) {
|
|
console.error('Fehler beim Laden der Benutzerstatistiken:', err);
|
|
this.error = err.response?.data?.error || 'Fehler beim Laden der Statistiken';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
getGenderPercentage(count) {
|
|
const total = Object.values(this.statistics.genderDistribution).reduce((sum, val) => sum + val, 0);
|
|
return total > 0 ? Math.round((count / total) * 100) : 0;
|
|
},
|
|
getAgePercentage(count) {
|
|
const total = Object.values(this.statistics.ageGroups).reduce((sum, val) => sum + val, 0);
|
|
return total > 0 ? Math.round((count / total) * 100) : 0;
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-statistics-view {
|
|
padding: 20px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
h2 {
|
|
margin-bottom: 30px;
|
|
color: #333;
|
|
}
|
|
|
|
.loading, .error {
|
|
text-align: center;
|
|
padding: 40px;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.error {
|
|
color: #d32f2f;
|
|
background-color: #ffebee;
|
|
border: 1px solid #ffcdd2;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.statistics-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 30px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stat-card h3 {
|
|
margin: 0 0 20px 0;
|
|
color: #333;
|
|
font-size: 20px;
|
|
border-bottom: 2px solid #f5f5f5;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.total-users {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 48px;
|
|
font-weight: bold;
|
|
color: #1976d2;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.gender-chart, .age-chart {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.gender-item, .age-item {
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr 100px;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.gender-label, .age-label {
|
|
font-weight: 500;
|
|
color: #555;
|
|
}
|
|
|
|
.gender-bar, .age-bar {
|
|
height: 24px;
|
|
background-color: #f0f0f0;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.gender-fill, .age-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #1976d2, #42a5f5);
|
|
border-radius: 12px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.gender-count, .age-count {
|
|
text-align: right;
|
|
font-weight: bold;
|
|
color: #333;
|
|
min-width: 40px;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.statistics-container {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.total-users {
|
|
grid-column: 1 / -1;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.statistics-container {
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
}
|
|
|
|
.total-users {
|
|
grid-column: 1 / -1;
|
|
}
|
|
}
|
|
</style>
|