Add pie chart to dashboard

This commit is contained in:
Elia Bieri 2024-09-12 13:10:53 +02:00
parent bd95776ec7
commit c65c1be0a8
7 changed files with 183 additions and 39 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { ArcElement, Chart as ChartJS } from "chart.js";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
import { Pie } from "vue-chartjs";
ChartJS.register(ArcElement);
const props = defineProps<{ data: Record<string, number> }>();
const { t } = useTranslation();
const data = computed(() => ({
labels: Object.entries(props.data).map(([key]) => key),
datasets: [
{
backgroundColor: Object.entries(props.data).map(
([key]) => CHOSEN_PROFILE_TO_COLOR[key]
),
data: Object.entries(props.data).map(([, value]) => value),
},
],
}));
const CHOSEN_PROFILE_TO_NAME: Record<string, string> = {
all: t("a.Allbranche"),
leben: t("a.Leben"),
nichtleben: t("a.Nichtleben"),
krankenzusatzversicherung: t("a.Krankenzusatzversicherungen"),
};
const CHOSEN_PROFILE_TO_COLOR: Record<string, string> = {
all: "#558AED",
leben: "#FE955A",
nichtleben: "#54CE8B",
krankenzusatzversicherung: "#FAC852",
};
</script>
w
<template>
<div class="flex flex-row items-center space-x-8">
<div class="size-32">
<Pie
:data="data"
:options="{
indexAxis: 'y',
responsive: true,
datasets: {
pie: {
borderWidth: 0,
},
},
}"
/>
</div>
<div class="flex flex-col space-y-2">
<div
v-for="(value, key) in props.data"
:key="key"
class="flex items-center space-x-2"
>
<div
class="h-4 w-4 rounded-full"
:style="{ backgroundColor: CHOSEN_PROFILE_TO_COLOR[key] }"
></div>
<span class="text-base font-bold">{{ value }}</span>
<span class="text-base">{{ CHOSEN_PROFILE_TO_NAME[key] }}</span>
</div>
</div>
</div>
</template>

View File

@ -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<string, number>
);
});
</script>
<template>
<div class="flex flex-col flex-wrap items-stretch md:flex-row">
<BaseBox
:details-link="`/dashboard/cost/${courseSessionId}`"
data-cy="dashboard.mentor.competenceSummary"
class="w-1/2"
>
<template #title>{{ $t("Kosten in") }} {{ new Date().getFullYear() }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div class="flex h-[74px] items-center justify-center py-1 pr-3">
<span class="text-3xl font-bold">
{{ formatCurrencyChfCentimes(totalCostInCurrentYear) }}
</span>
<span class="ml-4">CHF</span>
<div class="flex flex-col">
<div class="flex flex-col flex-wrap items-stretch md:flex-row">
<BaseBox
:details-link="`/dashboard/cost/${courseSessionId}`"
data-cy="dashboard.mentor.competenceSummary"
class="w-1/2"
>
<template #title>{{ $t("Kosten in") }} {{ new Date().getFullYear() }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div class="flex h-[74px] items-center justify-center py-1 pr-3">
<span class="text-3xl font-bold">
{{ formatCurrencyChfCentimes(totalCostInCurrentYear) }}
</span>
<span class="ml-4">CHF</span>
</div>
</div>
</div>
</template>
</BaseBox>
<BaseBox
:details-link="`/dashboard/persons?course=${courseId}`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("Teilnehmer im 2024") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
>
<span>{{ attendanceCountInCurrentYear }}</span>
</template>
</BaseBox>
<BaseBox
:details-link="`/dashboard/persons?course=${courseId}`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("Teilnehmer im 2024") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
>
<span>{{ attendanceCountInCurrentYear }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">
{{ $t("Teilnehmer") }}
</p>
</div>
<p class="ml-3 mt-0 leading-[74px]">
{{ $t("Teilnehmer") }}
</p>
</div>
</template>
</BaseBox>
</template>
</BaseBox>
</div>
<div class="my-6 space-y-6 border-t border-gray-500 py-6">
<h2 class="text-base font-bold">
{{ $t("Teilnehmer nach Zulassungsprofilen") }}
</h2>
<AttendancePerChosenProfileChart
v-if="attendanceCountPerChosenProfile"
:data="attendanceCountPerChosenProfile"
></AttendancePerChosenProfileChart>
</div>
</div>
</template>

View File

@ -14,7 +14,6 @@ import type {
CourseStatisticsType,
DashboardConfigType,
TrainingResponsibleStatisticsQuery,
TrainingResponsibleStatisticsType,
} from "@/gql/graphql";
import type {
DashboardPersonsPageMode,

View File

@ -1,4 +1,3 @@
from hashlib import md5
from typing import Dict, List, Set, Tuple
import graphene

View File

@ -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,
)