Add PerformanceCriteria page

This commit is contained in:
Daniel Egger 2023-09-13 16:00:06 +02:00
parent e5d6dd60f6
commit 5dfdd470ae
11 changed files with 188 additions and 19 deletions

View File

@ -2,7 +2,7 @@
<li
class="flex flex-col justify-between border-t border-gray-500 py-4 leading-[45px] lg:flex-row"
>
<div class="flex flex-row items-center md:w-1/4">
<div class="flex flex-row items-center lg:w-1/3">
<slot name="firstRow"></slot>
</div>
<div class="flex flex-1 items-center">

View File

@ -194,7 +194,7 @@ function setActiveClasses(translationKey: string) {
<it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking>
<p class="text-bold inline-block">
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id, circle).FAIL }}
</p>
</div>
@ -202,7 +202,7 @@ function setActiveClasses(translationKey: string) {
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<p class="text-bold inline-block">
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id, circle).SUCCESS }}
</p>
</li>
@ -210,7 +210,7 @@ function setActiveClasses(translationKey: string) {
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block">
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id, circle).UNKNOWN }}
</p>
</li>

View File

@ -37,8 +37,8 @@ onMounted(async () => {
</script>
<template>
<div class="container-large lg:mt-4">
<nav class="lg:pb-4">
<div class="container-large">
<nav class="py-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/course/${props.courseSlug}/competence/certificates`"

View File

@ -58,8 +58,8 @@ onMounted(async () => {
</script>
<template>
<div class="container-large lg:mt-4">
<h2 class="mb-4">{{ $t("a.Kompetenznachweise") }}</h2>
<div class="container-large">
<h2 class="mb-4 lg:py-4">{{ $t("a.Kompetenznachweise") }}</h2>
<div class="mb-4 bg-white p-8">
<div class="flex items-center">

View File

@ -168,13 +168,16 @@ const performanceCriteriaStatusCount = computed(() => {
</div>
</li>
</ul>
<!-- <router-link-->
<!-- :to="`${competenceStore.competenceProfilePage()?.frontend_url}-old/criteria`"-->
<!-- class="btn-text inline-flex items-center py-2 pl-0"-->
<!-- >-->
<!-- <span>{{ $t("general.showAll") }}</span>-->
<!-- <it-icon-arrow-right></it-icon-arrow-right>-->
<!-- </router-link>-->
<div>
<router-link
:to="`/course/${props.courseSlug}/competence/criteria`"
class="btn-text mt-4 inline-flex items-center py-2 pl-0"
>
<span>{{ $t("general.showAll") }}</span>
<it-icon-arrow-right></it-icon-arrow-right>
</router-link>
</div>
</section>
</div>
</template>

View File

@ -22,6 +22,10 @@ function routeInCompetenceCertificate() {
return route.path.includes("/certificate");
}
function routeInPerformanceCriteria() {
return route.path.endsWith("/criteria");
}
function routeInActionCompetences() {
return route.path.endsWith("/competences");
}
@ -58,6 +62,15 @@ onMounted(async () => {
{{ $t("a.Kompetenznachweise") }}
</router-link>
</li>
<li
class="ml-6 inline-block border-t-2 border-t-transparent py-3 lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInPerformanceCriteria() }"
>
<router-link :to="`/course/${courseSlug}/competence/criteria`">
{{ $t("a.Selbsteinschätzungen") }}
</router-link>
</li>
<li
class="ml-6 inline-block border-t-2 border-t-transparent py-3 lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { useCompetenceStore } from "@/stores/competence";
import * as log from "loglevel";
import { computed } from "vue";
import _ from "lodash";
const props = defineProps<{
courseSlug: string;
}>();
log.debug("PerformanceCriteriaPage created", props);
const competenceStore = useCompetenceStore();
const uniqueLearningUnits = computed(() => {
// FIXME: this complex calculation can go away,
// once the criteria are in its own learning content
// get the learningUnits sorted by circle order in the course
const circles = competenceStore.circles.map((c, index) => {
return { ...c, sortKey: index };
});
return _.orderBy(
_.uniqBy(
competenceStore.flatPerformanceCriteria().map((pc) => {
return {
luId: pc.learning_unit.id,
luTitle: pc.learning_unit.title,
circleId: pc.circle.id,
circleTitle: pc.circle.title,
url: pc.learning_unit.evaluate_url,
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
};
}),
"luId"
),
"sortKey"
);
});
const criteriaByLearningUnit = computed(() => {
return uniqueLearningUnits.value.map((lu) => {
const criteria = competenceStore
.flatPerformanceCriteria()
.filter((pc) => pc.learning_unit.id === lu.luId);
return {
...lu,
countSuccess: criteria.filter((c) => c.completion_status === "SUCCESS").length,
countFail: criteria.filter((c) => c.completion_status === "FAIL").length,
countUnknown: criteria.filter((c) => c.completion_status === "UNKNOWN").length,
criteria: criteria,
};
});
});
</script>
<template>
<div class="container-large">
<h2 class="mb-4 lg:py-4">{{ $t("a.Selbsteinschätzungen") }}</h2>
<section class="mb-4 bg-white px-4 py-2">
<div
v-for="selfEvaluation in criteriaByLearningUnit"
:key="selfEvaluation.luId"
class="flex items-center justify-between border-b py-4 last:border-b-0"
>
<div class="w-1/3">
{{ $t("a.Circle") }}
{{ selfEvaluation.circleTitle }}:
{{ selfEvaluation.luTitle }}
</div>
<div class="ml-4 flex w-1/3 flex-row items-center">
<div class="mr-6 flex flex-row items-center">
<it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking>
<div class="w-6">
{{ selfEvaluation.countFail }}
</div>
</div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<div class="w-6">
{{ selfEvaluation.countSuccess }}
</div>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<div class="w-6">
{{ selfEvaluation.countUnknown }}
</div>
</li>
</div>
<div>
<router-link :to="selfEvaluation.url" class="link">
{{ $t("a.Selbsteinschätzung anschauen") }}
</router-link>
</div>
</div>
</section>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,34 @@
// handle route history
import type {
NavigationGuard,
RouteLocationNormalized,
RouteLocationRaw,
Router,
} from "vue-router";
const routeHistory: RouteLocationNormalized[] = [];
const MAX_HISTORY = 10; // for example, store the last 10 visited routes
let isFirstNavigation = true;
export const addToHistory: NavigationGuard = (to, from, next) => {
// Add the current route to the history, and ensure it doesn't exceed the maximum length
if (isFirstNavigation) {
isFirstNavigation = false;
} else {
routeHistory.push(from);
}
if (routeHistory.length > MAX_HISTORY) {
routeHistory.shift();
}
next();
};
export function routerBackOrFallback(router: Router, fallbackRoute: RouteLocationRaw) {
// Check the latest route in history
const previousRoute = routeHistory[routeHistory.length - 1];
if (previousRoute) {
router.back();
} else {
router.push(fallbackRoute);
}
}

View File

@ -5,6 +5,7 @@ import {
redirectToLoginIfRequired,
updateLoggedIn,
} from "@/router/guards";
import { addToHistory } from "@/router/history";
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
@ -84,6 +85,11 @@ const router = createRouter({
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
path: "criteria",
props: true,
component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"),
},
{
path: "competences",
props: true,
@ -226,4 +232,6 @@ router.beforeEach(redirectToLoginIfRequired);
// register after login hooks
router.beforeEach(handleCourseSessions);
router.beforeEach(addToHistory);
export default router;

View File

@ -1,5 +1,4 @@
import * as log from "loglevel";
import { routerBackOrFallback } from "@/router/history";
import type { Circle } from "@/services/circle";
import { useCompletionStore } from "@/stores/completion";
import { useLearningPathStore } from "@/stores/learningPath";
@ -11,6 +10,7 @@ import type {
LearningUnitPerformanceCriteria,
PerformanceCriteria,
} from "@/types";
import * as log from "loglevel";
import { defineStore } from "pinia";
export type CircleStoreState = {
@ -128,7 +128,7 @@ export const useCircleStore = defineStore({
});
},
closeLearningContent(learningContent: LearningContentInterface) {
this.router.push({
routerBackOrFallback(this.router, {
path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningContent.parentLearningUnit),
});
@ -139,7 +139,7 @@ export const useCircleStore = defineStore({
});
},
closeSelfEvaluation(learningUnit: LearningUnit) {
this.router.push({
routerBackOrFallback(this.router, {
path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningUnit),
});

View File

@ -19,6 +19,7 @@ export type CompetenceStoreState = {
selectedCircle: { id: string; name: string };
availableCircles: { id: string; name: string }[];
circles: CircleLight[];
};
export const useCompetenceStore = defineStore({
@ -28,6 +29,7 @@ export const useCompetenceStore = defineStore({
competenceProfilePages: new Map<string, CompetenceProfilePage>(),
selectedCircle: { id: "all", name: `Circle: ${i18next.t("Alle")}` },
availableCircles: [],
circles: [],
} as CompetenceStoreState;
},
getters: {},
@ -153,6 +155,7 @@ export const useCompetenceStore = defineStore({
this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage));
this.circles = competenceProfilePage.circles;
const circles = competenceProfilePage.circles.map((c: CircleLight) => {
return { id: c.translation_key, name: `Circle: ${c.title}` };
});