From 8f4327efb560a2f41680e59157cacb0b2b5c1061 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 12 Sep 2025 16:34:56 +0200 Subject: [PATCH] =?UTF-8?q?=C3=84nderung:=20Erweiterung=20der=20Benutzerst?= =?UTF-8?q?atistiken=20im=20Admin-Bereich?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ä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. --- backend/controllers/adminController.js | 17 ++ backend/controllers/navigationController.js | 21 +- backend/models/community/user_param.js | 2 - backend/routers/adminRouter.js | 1 + backend/services/adminService.js | 106 +++++++++ frontend/src/i18n/locales/de/admin.json | 6 + frontend/src/i18n/locales/de/navigation.json | 8 +- frontend/src/i18n/locales/en/admin.json | 6 + frontend/src/i18n/locales/en/navigation.json | 15 +- frontend/src/router/adminRoutes.js | 7 + .../src/views/admin/UserStatisticsView.vue | 218 ++++++++++++++++++ 11 files changed, 394 insertions(+), 13 deletions(-) create mode 100644 frontend/src/views/admin/UserStatisticsView.vue diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index 27af914..b841b9b 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -34,6 +34,9 @@ class AdminController { this.listUserRights = this.listUserRights.bind(this); this.addUserRight = this.addUserRight.bind(this); this.removeUserRight = this.removeUserRight.bind(this); + + // Statistics + this.getUserStatistics = this.getUserStatistics.bind(this); } async getOpenInterests(req, res) { @@ -778,6 +781,20 @@ class AdminController { } } } + + async getUserStatistics(req, res) { + try { + const { userid: userId } = req.headers; + const statistics = await AdminService.getUserStatistics(userId); + res.status(200).json(statistics); + } catch (error) { + if (error.message === 'noaccess') { + res.status(403).json({ error: 'Keine Berechtigung' }); + } else { + res.status(500).json({ error: error.message }); + } + } + } } export default AdminController; diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index 8ced75d..a3b152c 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -222,9 +222,22 @@ const menuStructure = { visible: ["mainadmin", "contactrequests"], path: "/admin/contacts" }, - useradministration: { + users: { visible: ["mainadmin", "useradministration"], - path: "/admin/users" + children: { + userlist: { + visible: ["mainadmin", "useradministration"], + path: "/admin/users" + }, + userstatistics: { + visible: ["mainadmin"], + path: "/admin/users/statistics" + }, + userrights: { + visible: ["mainadmin", "rights"], + path: "/admin/rights" + } + } }, forum: { visible: ["mainadmin", "forum"], @@ -234,10 +247,6 @@ const menuStructure = { visible: ["mainadmin", "chatrooms"], path: "/admin/chatrooms" }, - userrights: { - visible: ["mainadmin", "rights"], - path: "/admin/rights" - }, interests: { visible: ["mainadmin", "interests"], path: "/admin/interests" diff --git a/backend/models/community/user_param.js b/backend/models/community/user_param.js index 219881d..73699f8 100644 --- a/backend/models/community/user_param.js +++ b/backend/models/community/user_param.js @@ -25,11 +25,9 @@ const UserParam = sequelize.define('user_param', { type: DataTypes.STRING, allowNull: false, set(value) { - console.log('.... [set param value]', value); if (value) { try { const encrypted = encrypt(value.toString()); - console.log('.... [encrypted param value]', encrypted); this.setDataValue('value', encrypted); } catch (error) { console.error('.... Error encrypting param value:', error); diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index deea3e6..e78a86e 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -17,6 +17,7 @@ router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom); // --- Users Admin --- router.get('/users/search', authenticate, adminController.searchUsers); +router.get('/users/statistics', authenticate, adminController.getUserStatistics); router.get('/users/:id', authenticate, adminController.getUser); router.put('/users/:id', authenticate, adminController.updateUser); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 0025037..7956c5e 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -21,6 +21,7 @@ import FalukantStockType from "../models/falukant/type/stock.js"; import RegionData from "../models/falukant/data/region.js"; import BranchType from "../models/falukant/type/branch.js"; import Room from '../models/chat/room.js'; +import UserParam from '../models/community/user_param.js'; class AdminService { async hasUserAccess(userId, section) { @@ -855,6 +856,111 @@ class AdminService { const Match3TileType = (await import('../models/match3/tileType.js')).default; return await Match3TileType.destroy({ where: { id } }); } + + async getUserStatistics(userId) { + if (!(await this.hasUserAccess(userId, 'mainadmin'))) { + throw new Error('noaccess'); + } + + // Gesamtanzahl angemeldeter Benutzer + const totalUsers = await User.count({ + where: { active: true } + }); + + // Geschlechterverteilung - ohne raw: true um Entschlüsselung zu ermöglichen + const genderStats = await UserParam.findAll({ + include: [{ + model: UserParamType, + as: 'paramType', + where: { description: 'gender' } + }] + }); + + const genderDistribution = {}; + for (const stat of genderStats) { + const genderId = stat.value; // Dies ist die ID des Geschlechts + if (genderId) { + const genderValue = await UserParamValue.findOne({ + where: { id: genderId } + }); + if (genderValue) { + const gender = genderValue.value; // z.B. 'male', 'female' + genderDistribution[gender] = (genderDistribution[gender] || 0) + 1; + } + } + } + + // Altersverteilung basierend auf Geburtsdatum - ohne raw: true um Entschlüsselung zu ermöglichen + const birthdateStats = await UserParam.findAll({ + include: [{ + model: UserParamType, + as: 'paramType', + where: { description: 'birthdate' } + }] + }); + + const ageGroups = { + 'unter 12': 0, + '12-14': 0, + '14-16': 0, + '16-18': 0, + '18-21': 0, + '21-25': 0, + '25-30': 0, + '30-40': 0, + '40-50': 0, + '50-60': 0, + 'über 60': 0 + }; + + const now = new Date(); + for (const stat of birthdateStats) { + try { + const birthdate = new Date(stat.value); + if (isNaN(birthdate.getTime())) continue; + + const age = now.getFullYear() - birthdate.getFullYear(); + const monthDiff = now.getMonth() - birthdate.getMonth(); + + let actualAge = age; + if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthdate.getDate())) { + actualAge--; + } + + if (actualAge < 12) { + ageGroups['unter 12']++; + } else if (actualAge >= 12 && actualAge < 14) { + ageGroups['12-14']++; + } else if (actualAge >= 14 && actualAge < 16) { + ageGroups['14-16']++; + } else if (actualAge >= 16 && actualAge < 18) { + ageGroups['16-18']++; + } else if (actualAge >= 18 && actualAge < 21) { + ageGroups['18-21']++; + } else if (actualAge >= 21 && actualAge < 25) { + ageGroups['21-25']++; + } else if (actualAge >= 25 && actualAge < 30) { + ageGroups['25-30']++; + } else if (actualAge >= 30 && actualAge < 40) { + ageGroups['30-40']++; + } else if (actualAge >= 40 && actualAge < 50) { + ageGroups['40-50']++; + } else if (actualAge >= 50 && actualAge < 60) { + ageGroups['50-60']++; + } else { + ageGroups['über 60']++; + } + } catch (error) { + console.error('Fehler beim Verarbeiten des Geburtsdatums:', error); + } + } + + return { + totalUsers, + genderDistribution, + ageGroups + }; + } } export default new AdminService(); \ No newline at end of file diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index 44f5160..76ff4e5 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -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" } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/de/navigation.json b/frontend/src/i18n/locales/de/navigation.json index 980f3b7..5244793 100644 --- a/frontend/src/i18n/locales/de/navigation.json +++ b/frontend/src/i18n/locales/de/navigation.json @@ -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": { diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index fa3060f..19743e3 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -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" } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/en/navigation.json b/frontend/src/i18n/locales/en/navigation.json index f1a8986..955c6ef 100644 --- a/frontend/src/i18n/locales/en/navigation.json +++ b/frontend/src/i18n/locales/en/navigation.json @@ -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", diff --git a/frontend/src/router/adminRoutes.js b/frontend/src/router/adminRoutes.js index a5c89e9..1841705 100644 --- a/frontend/src/router/adminRoutes.js +++ b/frontend/src/router/adminRoutes.js @@ -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', diff --git a/frontend/src/views/admin/UserStatisticsView.vue b/frontend/src/views/admin/UserStatisticsView.vue new file mode 100644 index 0000000..0e0f4bc --- /dev/null +++ b/frontend/src/views/admin/UserStatisticsView.vue @@ -0,0 +1,218 @@ + + + + +