Add pie chart to dashboard
This commit is contained in:
parent
bd95776ec7
commit
c65c1be0a8
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import type {
|
|||
CourseStatisticsType,
|
||||
DashboardConfigType,
|
||||
TrainingResponsibleStatisticsQuery,
|
||||
TrainingResponsibleStatisticsType,
|
||||
} from "@/gql/graphql";
|
||||
import type {
|
||||
DashboardPersonsPageMode,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from hashlib import md5
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
import graphene
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue