diff --git a/client/package-lock.json b/client/package-lock.json index 78e6e22a..f4917956 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,7 @@ "@vuepic/vue-datepicker": "^8.8.1", "@vueuse/core": "^10.11.0", "@vueuse/router": "^10.11.0", + "chart.js": "^4.4.4", "cypress": "^12.14.0", "d3": "^7.9.0", "dayjs": "^1.11.11", @@ -32,6 +33,7 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.31", + "vue-chartjs": "^5.3.1", "vue-router": "^4.4.0" }, "devDependencies": { @@ -2969,6 +2971,11 @@ "integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==", "dev": true }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6001,6 +6008,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -14249,6 +14267,15 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-component-type-helpers": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz", @@ -16854,6 +16881,11 @@ "integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==", "dev": true }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -18998,6 +19030,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -24648,6 +24688,11 @@ "@vue/shared": "3.4.31" } }, + "vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==" + }, "vue-component-type-helpers": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz", diff --git a/client/package.json b/client/package.json index f5296e99..9c2813cf 100644 --- a/client/package.json +++ b/client/package.json @@ -30,6 +30,7 @@ "@vuepic/vue-datepicker": "^8.8.1", "@vueuse/core": "^10.11.0", "@vueuse/router": "^10.11.0", + "chart.js": "^4.4.4", "cypress": "^12.14.0", "d3": "^7.9.0", "dayjs": "^1.11.11", @@ -43,6 +44,7 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.31", + "vue-chartjs": "^5.3.1", "vue-router": "^4.4.0" }, "devDependencies": { diff --git a/client/src/components/dashboard/AttendancePerChosenProfileChart.vue b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue new file mode 100644 index 00000000..07584563 --- /dev/null +++ b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue @@ -0,0 +1,71 @@ + +w + diff --git a/client/src/components/dashboard/TrainingResponsibleStatistics.vue b/client/src/components/dashboard/TrainingResponsibleStatistics.vue index d6a36054..5bef737c 100644 --- a/client/src/components/dashboard/TrainingResponsibleStatistics.vue +++ b/client/src/components/dashboard/TrainingResponsibleStatistics.vue @@ -3,6 +3,7 @@ import type { TrainingResponsibleStatisticsQuery } from "@/gql/graphql"; import { fetchTrainingResponsibleStatistics } from "@/services/dashboard"; import { formatCurrencyChfCentimes } from "@/utils/format_currency_chf"; import { computed, onMounted, ref } from "vue"; +import AttendancePerChosenProfileChart from "./AttendancePerChosenProfileChart.vue"; import BaseBox from "./BaseBox.vue"; const props = defineProps<{ @@ -29,44 +30,73 @@ const attendanceCountInCurrentYear = computed(() => { )?.participants?.length ?? 0 ); }); + +const attendanceCountPerChosenProfile = computed(() => { + const allAttendances = + statistics.value?.training_responsible_statistics?.participants_per_year?.flatMap( + (entry) => entry?.participants ?? [] + ); + return allAttendances?.reduce( + (acc, attendance) => { + const chosenProfile = attendance?.chosen_profile || "all"; + if (!acc[chosenProfile]) { + acc[chosenProfile] = 0; + } + acc[chosenProfile] += 1; + return acc; + }, + {} as Record + ); +}); diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 5045e37e..dccff8ed 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -14,7 +14,6 @@ import type { CourseStatisticsType, DashboardConfigType, TrainingResponsibleStatisticsQuery, - TrainingResponsibleStatisticsType, } from "@/gql/graphql"; import type { DashboardPersonsPageMode, diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 6bce8418..4eaf5589 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,4 +1,3 @@ -from hashlib import md5 from typing import Dict, List, Set, Tuple import graphene diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 4f9196a6..bdca5301 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,9 +1,8 @@ -from datetime import datetime from itertools import groupby + import graphene from graphene import Enum -from vbv_lernwelt.core.admin import User from vbv_lernwelt.course.graphql.types import ( CourseConfigurationObjectType, CourseSessionUserType, @@ -31,7 +30,6 @@ from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState from vbv_lernwelt.shop.views import ( COURSE_SESSION_ID_TO_PRODUCT_SKU, - PRODUCT_SKU_TO_COURSE_SESSION_ID, )