Änderung: Erweiterung der Benutzerstatistiken im Admin-Bereich

Ä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.
This commit is contained in:
Torsten Schulz (local)
2025-09-12 16:34:56 +02:00
parent b26bc0eb8b
commit 8f4327efb5
11 changed files with 394 additions and 13 deletions

View File

@@ -186,6 +186,12 @@
"objectiveDescriptionPlaceholder": "z.B. Sammle 100 Punkte",
"objectiveRequired": "Erforderlich für Level-Abschluss",
"noObjectives": "Keine Siegvoraussetzungen definiert. Klicke auf 'Objektiv hinzufügen' um welche zu erstellen."
},
"userStatistics": {
"title": "[Admin] - Benutzerstatistiken",
"totalUsers": "Gesamtanzahl Benutzer",
"genderDistribution": "Geschlechterverteilung",
"ageDistribution": "Altersverteilung"
}
}
}

View File

@@ -44,9 +44,13 @@
},
"m-administration": {
"contactrequests": "Kontaktanfragen",
"useradministration": "Benutzerverwaltung",
"users": "Benutzer",
"m-users": {
"userlist": "Benutzerliste",
"userstatistics": "Benutzerstatistiken",
"userrights": "Benutzerrechte"
},
"forum": "Forum",
"userrights": "Benutzerrechte",
"interests": "Interessen",
"falukant": "Falukant",
"m-falukant": {

View File

@@ -98,6 +98,12 @@
"stockAdded": "Warehouse successfully added.",
"invalidStockData": "Please enter valid warehouse type and quantity."
}
},
"userStatistics": {
"title": "[Admin] - User Statistics",
"totalUsers": "Total Users",
"genderDistribution": "Gender Distribution",
"ageDistribution": "Age Distribution"
}
}
}

View File

@@ -41,16 +41,25 @@
},
"m-administration": {
"contactrequests": "Contact requests",
"useradministration": "User administration",
"users": "Users",
"m-users": {
"userlist": "User list",
"userstatistics": "User statistics",
"userrights": "User rights"
},
"forum": "Forum",
"userrights": "User rights",
"interests": "Interests",
"falukant": "Falukant",
"m-falukant": {
"logentries": "Log entries",
"edituser": "Edit user",
"database": "Database"
}
},
"minigames": "Mini games",
"m-minigames": {
"match3": "Match3 Levels"
},
"chatrooms": "Chat rooms"
},
"m-friends": {
"manageFriends": "Manage friends",

View File

@@ -6,6 +6,7 @@ import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue';
import AdminMinigamesView from '../views/admin/MinigamesView.vue';
import AdminUsersView from '../views/admin/UsersView.vue';
import UserStatisticsView from '../views/admin/UserStatisticsView.vue';
const adminRoutes = [
{
@@ -20,6 +21,12 @@ const adminRoutes = [
component: AdminUsersView,
meta: { requiresAuth: true }
},
{
path: '/admin/users/statistics',
name: 'AdminUserStatistics',
component: UserStatisticsView,
meta: { requiresAuth: true }
},
{
path: '/admin/contacts',
name: 'AdminContacts',

View File

@@ -0,0 +1,218 @@
<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>