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 <li
class="flex flex-col justify-between border-t border-gray-500 py-4 leading-[45px] lg:flex-row" 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> <slot name="firstRow"></slot>
</div> </div>
<div class="flex flex-1 items-center"> <div class="flex flex-1 items-center">

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,10 @@ function routeInCompetenceCertificate() {
return route.path.includes("/certificate"); return route.path.includes("/certificate");
} }
function routeInPerformanceCriteria() {
return route.path.endsWith("/criteria");
}
function routeInActionCompetences() { function routeInActionCompetences() {
return route.path.endsWith("/competences"); return route.path.endsWith("/competences");
} }
@ -58,6 +62,15 @@ onMounted(async () => {
{{ $t("a.Kompetenznachweise") }} {{ $t("a.Kompetenznachweise") }}
</router-link> </router-link>
</li> </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 <li
class="ml-6 inline-block border-t-2 border-t-transparent py-3 lg:ml-12" 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() }" :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, redirectToLoginIfRequired,
updateLoggedIn, updateLoggedIn,
} from "@/router/guards"; } from "@/router/guards";
import { addToHistory } from "@/router/history";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({ const router = createRouter({
@ -84,6 +85,11 @@ const router = createRouter({
component: () => component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"), import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
}, },
{
path: "criteria",
props: true,
component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"),
},
{ {
path: "competences", path: "competences",
props: true, props: true,
@ -226,4 +232,6 @@ router.beforeEach(redirectToLoginIfRequired);
// register after login hooks // register after login hooks
router.beforeEach(handleCourseSessions); router.beforeEach(handleCourseSessions);
router.beforeEach(addToHistory);
export default router; 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 type { Circle } from "@/services/circle";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
@ -11,6 +10,7 @@ import type {
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import * as log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CircleStoreState = { export type CircleStoreState = {
@ -128,7 +128,7 @@ export const useCircleStore = defineStore({
}); });
}, },
closeLearningContent(learningContent: LearningContentInterface) { closeLearningContent(learningContent: LearningContentInterface) {
this.router.push({ routerBackOrFallback(this.router, {
path: `${this.circle?.frontend_url}`, path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningContent.parentLearningUnit), hash: createLearningUnitHash(learningContent.parentLearningUnit),
}); });
@ -139,7 +139,7 @@ export const useCircleStore = defineStore({
}); });
}, },
closeSelfEvaluation(learningUnit: LearningUnit) { closeSelfEvaluation(learningUnit: LearningUnit) {
this.router.push({ routerBackOrFallback(this.router, {
path: `${this.circle?.frontend_url}`, path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningUnit), hash: createLearningUnitHash(learningUnit),
}); });

View File

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