Add pie chart to dashboard
This commit is contained in:
parent
bd95776ec7
commit
c65c1be0a8
|
|
@ -19,6 +19,7 @@
|
||||||
"@vuepic/vue-datepicker": "^8.8.1",
|
"@vuepic/vue-datepicker": "^8.8.1",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"@vueuse/router": "^10.11.0",
|
"@vueuse/router": "^10.11.0",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
"cypress": "^12.14.0",
|
"cypress": "^12.14.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
|
@ -32,6 +33,7 @@
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
|
"vue-chartjs": "^5.3.1",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -2969,6 +2971,11 @@
|
||||||
"integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==",
|
"integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -6001,6 +6008,17 @@
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"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": {
|
"node_modules/vue-component-type-helpers": {
|
||||||
"version": "2.0.26",
|
"version": "2.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz",
|
"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==",
|
"integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==",
|
||||||
"dev": true
|
"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": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -18998,6 +19030,14 @@
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"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": {
|
"check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
|
|
@ -24648,6 +24688,11 @@
|
||||||
"@vue/shared": "3.4.31"
|
"@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": {
|
"vue-component-type-helpers": {
|
||||||
"version": "2.0.26",
|
"version": "2.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz",
|
"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",
|
"@vuepic/vue-datepicker": "^8.8.1",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"@vueuse/router": "^10.11.0",
|
"@vueuse/router": "^10.11.0",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
"cypress": "^12.14.0",
|
"cypress": "^12.14.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
|
"vue-chartjs": "^5.3.1",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 { fetchTrainingResponsibleStatistics } from "@/services/dashboard";
|
||||||
import { formatCurrencyChfCentimes } from "@/utils/format_currency_chf";
|
import { formatCurrencyChfCentimes } from "@/utils/format_currency_chf";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import AttendancePerChosenProfileChart from "./AttendancePerChosenProfileChart.vue";
|
||||||
import BaseBox from "./BaseBox.vue";
|
import BaseBox from "./BaseBox.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -29,44 +30,73 @@ const attendanceCountInCurrentYear = computed(() => {
|
||||||
)?.participants?.length ?? 0
|
)?.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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-wrap items-stretch md:flex-row">
|
<div class="flex flex-col">
|
||||||
<BaseBox
|
<div class="flex flex-col flex-wrap items-stretch md:flex-row">
|
||||||
:details-link="`/dashboard/cost/${courseSessionId}`"
|
<BaseBox
|
||||||
data-cy="dashboard.mentor.competenceSummary"
|
:details-link="`/dashboard/cost/${courseSessionId}`"
|
||||||
class="w-1/2"
|
data-cy="dashboard.mentor.competenceSummary"
|
||||||
>
|
class="w-1/2"
|
||||||
<template #title>{{ $t("Kosten in") }} {{ new Date().getFullYear() }}</template>
|
>
|
||||||
<template #content>
|
<template #title>{{ $t("Kosten in") }} {{ new Date().getFullYear() }}</template>
|
||||||
<div class="flex flex-row space-x-3 bg-white pb-6">
|
<template #content>
|
||||||
<div class="flex h-[74px] items-center justify-center py-1 pr-3">
|
<div class="flex flex-row space-x-3 bg-white pb-6">
|
||||||
<span class="text-3xl font-bold">
|
<div class="flex h-[74px] items-center justify-center py-1 pr-3">
|
||||||
{{ formatCurrencyChfCentimes(totalCostInCurrentYear) }}
|
<span class="text-3xl font-bold">
|
||||||
</span>
|
{{ formatCurrencyChfCentimes(totalCostInCurrentYear) }}
|
||||||
<span class="ml-4">CHF</span>
|
</span>
|
||||||
|
<span class="ml-4">CHF</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</BaseBox>
|
||||||
</BaseBox>
|
<BaseBox
|
||||||
<BaseBox
|
:details-link="`/dashboard/persons?course=${courseId}`"
|
||||||
:details-link="`/dashboard/persons?course=${courseId}`"
|
data-cy="dashboard.mentor.competenceSummary"
|
||||||
data-cy="dashboard.mentor.competenceSummary"
|
>
|
||||||
>
|
<template #title>{{ $t("Teilnehmer im 2024") }}</template>
|
||||||
<template #title>{{ $t("Teilnehmer im 2024") }}</template>
|
<template #content>
|
||||||
<template #content>
|
<div class="flex flex-row space-x-3 bg-white pb-6">
|
||||||
<div class="flex flex-row space-x-3 bg-white pb-6">
|
<div
|
||||||
<div
|
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
|
||||||
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
|
>
|
||||||
>
|
<span>{{ attendanceCountInCurrentYear }}</span>
|
||||||
<span>{{ attendanceCountInCurrentYear }}</span>
|
</div>
|
||||||
|
<p class="ml-3 mt-0 leading-[74px]">
|
||||||
|
{{ $t("Teilnehmer") }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="ml-3 mt-0 leading-[74px]">
|
</template>
|
||||||
{{ $t("Teilnehmer") }}
|
</BaseBox>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="my-6 space-y-6 border-t border-gray-500 py-6">
|
||||||
</template>
|
<h2 class="text-base font-bold">
|
||||||
</BaseBox>
|
{{ $t("Teilnehmer nach Zulassungsprofilen") }}
|
||||||
|
</h2>
|
||||||
|
<AttendancePerChosenProfileChart
|
||||||
|
v-if="attendanceCountPerChosenProfile"
|
||||||
|
:data="attendanceCountPerChosenProfile"
|
||||||
|
></AttendancePerChosenProfileChart>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import type {
|
||||||
CourseStatisticsType,
|
CourseStatisticsType,
|
||||||
DashboardConfigType,
|
DashboardConfigType,
|
||||||
TrainingResponsibleStatisticsQuery,
|
TrainingResponsibleStatisticsQuery,
|
||||||
TrainingResponsibleStatisticsType,
|
|
||||||
} from "@/gql/graphql";
|
} from "@/gql/graphql";
|
||||||
import type {
|
import type {
|
||||||
DashboardPersonsPageMode,
|
DashboardPersonsPageMode,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from hashlib import md5
|
|
||||||
from typing import Dict, List, Set, Tuple
|
from typing import Dict, List, Set, Tuple
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from datetime import datetime
|
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import Enum
|
from graphene import Enum
|
||||||
|
|
||||||
from vbv_lernwelt.core.admin import User
|
|
||||||
from vbv_lernwelt.course.graphql.types import (
|
from vbv_lernwelt.course.graphql.types import (
|
||||||
CourseConfigurationObjectType,
|
CourseConfigurationObjectType,
|
||||||
CourseSessionUserType,
|
CourseSessionUserType,
|
||||||
|
|
@ -31,7 +30,6 @@ from vbv_lernwelt.learnpath.models import Circle
|
||||||
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
|
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
|
||||||
from vbv_lernwelt.shop.views import (
|
from vbv_lernwelt.shop.views import (
|
||||||
COURSE_SESSION_ID_TO_PRODUCT_SKU,
|
COURSE_SESSION_ID_TO_PRODUCT_SKU,
|
||||||
PRODUCT_SKU_TO_COURSE_SESSION_ID,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue