Merge branch 'develop'

This commit is contained in:
Christian Cueni 2023-02-06 09:00:56 +01:00
commit 34e59a433c
17 changed files with 277 additions and 87 deletions

View File

@ -1,7 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { loadLocaleMessages, setI18nLanguage } from "@/i18n";
import * as log from "loglevel"; import * as log from "loglevel";
log.debug("AppFooter created"); log.debug("AppFooter created");
async function changeLocale(event: Event) {
const target = event.target as HTMLSelectElement;
await loadLocaleMessages(target.value);
setI18nLanguage(target.value);
}
</script> </script>
<template> <template>
@ -13,6 +20,11 @@ log.debug("AppFooter created");
<div class="flex-grow"></div> <div class="flex-grow"></div>
<div>VBV_VERSION_BUILD_NUMBER_VBV</div> <div>VBV_VERSION_BUILD_NUMBER_VBV</div>
<div class="lg:ml-8">Deutsch</div> <div class="lg:ml-8">Deutsch</div>
<!--div class="locale-changer">
<select @change="changeLocale($event)">
<option v-for="locale in ['de', 'fr']" :key="`locale-${locale}`" :value="locale">{{ locale }}</option>
</select>
</div-->
<div class="lg:ml-8">{{ $t("footer.contact") }}</div> <div class="lg:ml-8">{{ $t("footer.contact") }}</div>
</footer> </footer>
</template> </template>

View File

@ -2,7 +2,7 @@
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import type { DefaultArcObject } from "d3"; import type { DefaultArcObject } from "d3";
import * as d3 from "d3"; import * as d3 from "d3";
import * as _ from "lodash"; import pick from "lodash/pick";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
@ -64,7 +64,7 @@ const pieData = computed(() => {
{ {
startAngle: angle.startAngle, startAngle: angle.startAngle,
endAngle: angle.endAngle, endAngle: angle.endAngle,
..._.pick(thisLearningSequence, ["title", "icon", "translation_key", "slug"]), ...pick(thisLearningSequence, ["title", "icon", "translation_key", "slug"]),
arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2, arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2,
arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2, arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
someFinished: someFinished(thisLearningSequence), someFinished: someFinished(thisLearningSequence),

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import * as d3 from "d3"; import * as d3 from "d3";
import * as _ from "lodash"; import kebabCase from "lodash/kebabCase";
import * as log from "loglevel"; import * as log from "loglevel";
// @ts-ignore // @ts-ignore
@ -12,7 +12,7 @@ import type { LearningSequence, Topic } from "@/types";
import { computed, onMounted, reactive, watch } from "vue"; import { computed, onMounted, reactive, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
export type DiagramType = "horizontal" | "vertical" | "horizontalSmall"; export type DiagramType = "horizontal" | "vertical" | "horizontalSmall" | "singleSmall";
export interface Props { export interface Props {
diagramType?: DiagramType; diagramType?: DiagramType;
@ -21,6 +21,7 @@ export interface Props {
learningPath: LearningPath; learningPath: LearningPath;
// set to undefined (default) to show all circles // set to undefined (default) to show all circles
showCircleTranslationKeys: string[]; showCircleTranslationKeys: string[];
pullUp?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -28,6 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
postfix: "", postfix: "",
profileUserId: "", profileUserId: "",
showCircleTranslationKeys: undefined, showCircleTranslationKeys: undefined,
pullUp: true,
}); });
log.debug( log.debug(
@ -37,14 +39,14 @@ log.debug(
props.showCircleTranslationKeys props.showCircleTranslationKeys
); );
const state = reactive({ width: 1640, height: 384 }); const state = reactive({ width: 1640, height: 384, startY: 0 });
const svgId = computed(() => { const svgId = computed(() => {
return `learningpath-diagram-${props.learningPath?.slug}-${props.diagramType}${props.postfix}`; return `learningpath-diagram-${props.learningPath?.slug}-${props.diagramType}${props.postfix}`;
}); });
const viewBox = computed(() => { const viewBox = computed(() => {
return `0 0 ${state.width} ${state.height}`; return `0 ${state.startY} ${state.width} ${state.height}`;
}); });
const vueRouter = useRouter(); const vueRouter = useRouter();
@ -135,7 +137,7 @@ const circles = computed(() => {
frontend_url: circle.frontend_url, frontend_url: circle.frontend_url,
id: circle.id, id: circle.id,
translation_key: circle.translation_key, translation_key: circle.translation_key,
slug: _.kebabCase(circle.title), slug: kebabCase(circle.title),
}); });
} }
}); });
@ -164,6 +166,10 @@ function render() {
if (props.diagramType === "vertical") { if (props.diagramType === "vertical") {
state.width = Math.min(960, window.innerWidth - 32); state.width = Math.min(960, window.innerWidth - 32);
state.height = 860; state.height = 860;
} else if (props.diagramType === "singleSmall") {
state.height = 260;
state.startY = 60;
state.width = circleWidth * circles.value.length;
} else { } else {
state.width = circleWidth * circles.value.length; state.width = circleWidth * circles.value.length;
} }
@ -466,10 +472,11 @@ function render() {
<div class="svg-container h-full content-start"> <div class="svg-container h-full content-start">
<svg <svg
:id="svgId" :id="svgId"
class="learning-path-visualization mx-auto -mt-6 h-full lg:mt-0" class="learning-path-visualization mx-auto h-full lg:mt-0"
:class="{ :class="{
'max-h-[90px]': ['horizontalSmall'].includes(diagramType), 'max-h-[90px]': ['horizontalSmall', 'singleSmall'].includes(diagramType),
'max-h-[90px] lg:max-h-[380px]': ['horizontal'].includes(diagramType), 'max-h-[90px] lg:max-h-[380px]': ['horizontal'].includes(diagramType),
'-mt-6': pullUp,
}" }"
:viewBox="viewBox" :viewBox="viewBox"
></svg> ></svg>

View File

@ -7,7 +7,7 @@ import type {
LearningSequence, LearningSequence,
} from "@/types"; } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration"; import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash"; import findLast from "lodash/findLast";
import { computed } from "vue"; import { computed } from "vue";
import LearningContentBadge from "./LearningContentTypeBadge.vue"; import LearningContentBadge from "./LearningContentTypeBadge.vue";
@ -52,7 +52,7 @@ const allFinished = computed(() => {
const continueTranslationKeyTuple = computed(() => { const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
const lastFinished = _.findLast( const lastFinished = findLast(
circleStore.circle.flatLearningContents, circleStore.circle.flatLearningContents,
(learningContent) => { (learningContent) => {
return learningContent.completion_status === "success"; return learningContent.completion_status === "success";

View File

@ -8,7 +8,7 @@
<div class="flex flex-1 items-center"> <div class="flex flex-1 items-center">
<slot name="center"></slot> <slot name="center"></slot>
</div> </div>
<div class="flex items-center md:w-1/4"> <div class="flex items-center lg:w-1/4">
<slot name="link"></slot> <slot name="link"></slot>
</div> </div>
</li> </li>

View File

@ -3,14 +3,15 @@ import { createI18n } from "vue-i18n";
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html // https://vue-i18n.intlify.dev/guide/advanced/lazy.html
export const SUPPORT_LOCALES = ["de", "fr", "it"]; export const SUPPORT_LOCALES = ["de", "fr", "it"];
let i18n: any = null;
export function setupI18n(options = { locale: "de", legacy: false }) { export function setupI18n(options = { locale: "de", legacy: false }) {
const i18n = createI18n(options); i18n = createI18n(options);
setI18nLanguage(i18n, options.locale); setI18nLanguage(options.locale);
return i18n; return i18n;
} }
export function setI18nLanguage(i18n: any, locale: string) { export function setI18nLanguage(locale: string) {
if (i18n.mode === "legacy") { if (i18n.mode === "legacy") {
i18n.global.locale = locale; i18n.global.locale = locale;
} else { } else {
@ -26,7 +27,7 @@ export function setI18nLanguage(i18n: any, locale: string) {
document.querySelector("html")?.setAttribute("lang", locale); document.querySelector("html")?.setAttribute("lang", locale);
} }
export async function loadLocaleMessages(i18n: any, locale: any) { export async function loadLocaleMessages(locale: any) {
// load locale messages with dynamic import // load locale messages with dynamic import
const messages = await import( const messages = await import(
/* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json` /* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json`

159
client/src/locales/fr.json Normal file
View File

@ -0,0 +1,159 @@
{
"general": {
"nextStep": "Prochaine étape",
"start": "Commencer",
"backToLearningPath": "zurück zum Lernpfad",
"backToCircle": "zurück zum Circle",
"next": "Weiter",
"back": "zurück",
"backCapitalized": "@.capitalize:general.back",
"save": "Speichern",
"learningUnit": "Lerneinheit",
"learningPath": "Lernpfad",
"learningSequence": "Lernsequenz",
"show": "Anschauen",
"circles": "Circles",
"transferTask": "Transferauftrag | Transferaufträge",
"feedback": "Feedback | Feedbacks",
"exam": "Prüfung | Prüfungen",
"examResult": "Prüfungsresultat | Prüfungsresultate",
"certificate": "Zertifikat | Zertifikate",
"notification": "Benachrichtigung | Benachrichtigungen",
"profileLink": "Profil anzeigen",
"shop": "Shop",
"yes": "Ja",
"no": "Nein"
},
"mainNavigation": {
"logout": "Abmelden",
"settings": "Kontoeinstellungen"
},
"dashboard": {
"welcome": "Willkommen, {name}"
},
"learningPathPage": {
"welcomeBack": "Willkommen zurück, {name}",
"showListView": "Listenansicht anzeigen",
"nextStep": "Nächster Schritt"
},
"circlePage": {
"duration": "Dauer",
"circleContentBoxTitle": "Das lernst du in diesem Circle.",
"gotQuestions": "Hast du Fragen?",
"contactExpertButton": "Fachexpertin kontaktieren",
"contactExpertDescription": "Tausche dich mit der Fachexpertin für den Circle {circleName} aus.",
"learnMore": "Erfahre mehr dazu",
"documents": {
"title": "Unterlagen",
"expertDescription": "Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.",
"userDescription": "Hier findest du die Unterlagen, die dir die Fachexpertin zur Verfügung gestellt hat.",
"action": "Unterlagen hochladen",
"modalAction": "Datei auswählen",
"fileLabel": "Datei",
"modalFileName": "Name",
"modalNameInformation": "Max. 70 Zeichen",
"chooseSequence": "Wähle eine Lernsequenz aus",
"selectFile": "Bitte wähle eine Datei aus",
"chooseName": "Bitte wähle einen Namen",
"chooseLearningSequence": "Bitte wähle eine Lernsequenz aus",
"uploadErrorMessage": "Beim Hochladen ist ein Fehler aufgetreten. Bitte versuche es erneut.",
"maxFileSize": "Maximale Dateigrösse: 20 MB"
}
},
"learningContent": {
"completeAndContinue": "Als erledigt markieren"
},
"selfEvaluation": {
"selfEvaluation": "Selbsteinschätzung",
"title": "@:selfEvaluation.selfEvaluation {title}",
"steps": "Schritt {current} von {max}",
"instruction": [
"Überprüfe, ob du in der Lernheinheit",
"alles verstanden hast.",
"Lies die folgende Aussage und bewerte sie:"
],
"yes": "Ja, ich kann das",
"no": "Das muss ich nochmals anschauen",
"progressText": "Schau dein Fortschritt in deinem KompetenzNavi:",
"progressLink": "KompetenzNavi öffnen",
"selfEvaluationYes": "@:selfEvaluation: Ich kann das.",
"selfEvaluationNo": "@:selfEvaluation: Muss ich nochmals anschauen."
},
"competences": {
"competences": "Kompetenzen",
"title": "KompetenzNavi",
"lastImprovements": "Letzte verbesserte Kompetenzen",
"showAll": "Alle anschauen",
"assessment": "Einschätzungen",
"notAssessed": "Nicht eingeschätzt",
"assessAgain": "Sich nochmals einschätzen"
},
"mediaLibrary": {
"title": "Mediathek",
"learningMedia": {
"titel": "Lernmedien",
"description": "Finde eine vollständige Liste der Bücher und anderen Medien, auf die im Kurs verwiesen wird."
},
"handlungsfelder": {
"title": "Handlungsfeld | Handlungsfelder",
"description": "Finde alle Ressourcen der Handlungsfelder wie Lernmedien, Links und andere nützliche Informationen."
}
},
"footer": {
"dataProtection": "Datenschutzbestimmungen",
"imprint": "Impressum",
"contact": "Kontakt",
"faq": "FAQ"
},
"cockpit": {
"title": "Cockpit",
"tasksDone": "Erledigte Transferaufträge von Teilnehmer.",
"feedbacksDone": "Abgeschickte Feedbacks von Teilnehmer.",
"examsDone": "Abgelegte Prüfungen von Teilnehmer.",
"progress": "Fortschritt",
"profileLink": "Profil anzeigen"
},
"messages": {
"sendMessage": "Nachricht schreiben"
},
"feedback": {
"intro": "{name}, dein/e Trainer/-in, bittet dich, ihm/ihr Feedback zu geben. Das ist freiwillig, würde aber ihm/ihr helfen, deine Lernerlebniss zu verbessern.",
"areYouSatisfied": "Wie zufrieden bist du?",
"recommendLabel": "Würden Sie den Kurs weiterempfehlen?",
"satisfactionLabel": "Zufriedenheit insgesamt",
"goalAttainmentLabel": "Zielerreichung insgesamt",
"proficiencyLabel": "Wie beurteilen Sie Ihre Sicherheit bezüglichen den Themen nach dem Kurs?",
"receivedMaterialsLabel": "Haben Sie Vorbereitungsunterlagen (z.B. eLearning) erhalten?",
"materialsRatingLabel": "Falls ja: Wie beurteilen Sie die Vorbereitungsunterlagen (z.B. eLearning)?",
"instructorCompetenceLabel": "Der Kursleiter war themenstark, fachkompetent.",
"instructorRespectLabel": "Fragen und Anregungen der Kursteilnehmenden wurden ernst genommen und aufgegriffen.",
"instructorOpenFeedbackLabel": "Was ich dem Kursleiter sonst noch sagen wollte:",
"courseNegativeFeedbackLabel": "Wo sehen Sie Verbesserungspotenzial?",
"coursePositiveFeedbackLabel": "Was hat Ihnen besonders gut gefallen?",
"completionTitle": "Schicke dein Feedback an {name}",
"completionDescription": "Dein Feedback ist anonym. Dein Vor- und Nachname werden bei deiner Trainer/-in nicht angezeigt.",
"sendFeedback": "Feedback abschicken",
"feedbackSent": "Dein Feedback wurde abgeschickt",
"circleFeedback": "Feedback zum Circle",
"showDetails": "Details anzeigen",
"sentByUsers": "Von {count} Teilnehmern ausgefüllt",
"feedbackPageTitle": "Feedback zum Lehrgang",
"feedbackPageInfo": "Teilnehmer haben das Feedback ausgefüllt",
"questionTitle": "Frage",
"veryUnhappy": "Sehr unzufrieden",
"unhappy": "Unzufrieden",
"happy": "Zufrieden",
"veryHappy": "Sehr zufrieden",
"average": "Durchschnitt",
"answers": "Antworten",
"noFeedbacks": "Es wurden noch keine Feedbacks abgegeben"
},
"constants": {
"yes": "Ja",
"no": "Nein",
"verySatisfied": "sehr zufrieden",
"satisfied": "zufrieden",
"unsatisfied": "unzufrieden",
"veryUnsatisfied": "sehr unzufrieden"
}
}

View File

@ -39,7 +39,7 @@ Sentry.init({
}); });
// todo: define lang setup // todo: define lang setup
loadLocaleMessages(i18n, "de").then(() => { loadLocaleMessages("de").then(() => {
app.use(router); app.use(router);
const pinia = createPinia(); const pinia = createPinia();

View File

@ -10,6 +10,7 @@ import { useCompetenceStore } from "@/stores/competence";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import groupBy from "lodash/groupBy";
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
@ -25,10 +26,14 @@ const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSessionStore = useCourseSessionsStore(); const courseSessionStore = useCourseSessionsStore();
function userCountStatus(userId: number) { function userCountStatusForCircle(userId: number, translationKey: string) {
return competenceStore.calcStatusCount( const criteria = competenceStore.flatPerformanceCriteria(
competenceStore.flatPerformanceCriteria(userId, cockpitStore.selectedCircles) userId,
cockpitStore.selectedCircles
); );
const grouped = groupBy(criteria, "circle.translation_key");
return competenceStore.calcStatusCount(grouped[translationKey]);
} }
const circles = computed(() => { const circles = computed(() => {
@ -141,9 +146,15 @@ function setActiveClasses(translationKey: string) {
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
> >
<template #center> <template #center>
<div class="flex flex-row items-center justify-between"> <div
<div class="mr-6 flex flex-row items-center gap-4"> class="mt-2 flex w-full flex-row items-center justify-between lg:mt-0"
<div class="h-12"> >
<ul class="w-full">
<li
class="flex h-12 items-center justify-between"
v-for="(circle, i) of cockpitStore.selectedCircles"
:key="i"
>
<LearningPathDiagram <LearningPathDiagram
v-if=" v-if="
learningPathStore.learningPathForUser( learningPathStore.learningPathForUser(
@ -157,44 +168,44 @@ function setActiveClasses(translationKey: string) {
csu.user_id csu.user_id
) as LearningPath ) as LearningPath
" "
:postfix="`cockpit-${csu.user_id}`" :postfix="`cockpit-${csu.user_id}-${i}`"
:profile-user-id="`${csu.user_id}`" :profile-user-id="`${csu.user_id}`"
:show-circle-translation-keys="cockpitStore.selectedCircles" :show-circle-translation-keys="[circle]"
diagram-type="horizontalSmall" :pull-up="false"
diagram-type="singleSmall"
class="mr-4"
></LearningPathDiagram> ></LearningPathDiagram>
</div> <p class="lg:min-w-[150px]">
<div> {{ selectedCirclesTitles[i] }}
<span v-for="title in selectedCirclesTitles" :key="title">
{{ title }}
</span>
</div>
</div>
<div class="ml-4 flex 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>
<p class="text-bold inline-block">
{{ userCountStatus(csu.user_id).fail }}
</p>
</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>
<p class="text-bold inline-block">
{{ userCountStatus(csu.user_id).success }}
</p> </p>
<div class="ml-4 flex 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>
<p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).fail }}
</p>
</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>
<p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).success }}
</p>
</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>
<p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).unknown }}
</p>
</li>
</div>
</li> </li>
<li class="flex flex-row items-center"> </ul>
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block">
{{ userCountStatus(csu.user_id).unknown }}
</p>
</li>
</div>
</div> </div>
</template> </template>
<template #link> <template #link>

View File

@ -3,7 +3,8 @@ import PerformanceCriteriaRow from "@/components/competences/PerformanceCriteria
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import _ from "lodash"; import maxBy from "lodash/maxBy";
import orderBy from "lodash/orderBy";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
@ -25,7 +26,7 @@ const failedCriteria = computed(() => {
}); });
const lastUpdatedCompetences = computed(() => { const lastUpdatedCompetences = computed(() => {
return _.orderBy( return orderBy(
competenceStore.competences(), competenceStore.competences(),
[ [
(competence) => { (competence) => {
@ -38,7 +39,7 @@ const lastUpdatedCompetences = computed(() => {
}); });
} }
return ( return (
_.maxBy(criteria, "completion_status_updated_at") maxBy(criteria, "completion_status_updated_at")
?.completion_status_updated_at || "" ?.completion_status_updated_at || ""
); );
}, },

View File

@ -13,7 +13,7 @@ import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSessionUser, DocumentUploadData } from "@/types"; import type { CourseSessionUser, DocumentUploadData } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration"; import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash"; import sumBy from "lodash/sumBy";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
const route = useRoute(); const route = useRoute();
@ -44,7 +44,7 @@ const circleStore = useCircleStore();
const duration = computed(() => { const duration = computed(() => {
if (circleStore.circle) { if (circleStore.circle) {
const minutes = _.sumBy(circleStore.circle.learningSequences, "minutes"); const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
return humanizeDuration(minutes); return humanizeDuration(minutes);
} }

View File

@ -10,7 +10,9 @@ import type {
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
WagtailCircle, WagtailCircle,
} from "@/types"; } from "@/types";
import _ from "lodash"; import groupBy from "lodash/groupBy";
import partition from "lodash/partition";
import values from "lodash/values";
export function parseLearningSequences( export function parseLearningSequences(
circle: Circle, circle: Circle,
@ -192,7 +194,7 @@ export class Circle implements WagtailCircle {
public allFinishedInLearningSequence(translationKey: string): boolean { public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) { if (translationKey) {
const [performanceCriteria, learningContents] = _.partition( const [performanceCriteria, learningContents] = partition(
this.flatChildren.filter( this.flatChildren.filter(
(lc) => lc.parentLearningSequence?.translation_key === translationKey (lc) => lc.parentLearningSequence?.translation_key === translationKey
), ),
@ -201,8 +203,8 @@ export class Circle implements WagtailCircle {
} }
); );
const groupedPerformanceCriteria = _.values( const groupedPerformanceCriteria = values(
_.groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id) groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
); );
return ( return (

View File

@ -1,4 +1,4 @@
import * as _ from "lodash"; import orderBy from "lodash/orderBy";
import { Circle } from "@/services/circle"; import { Circle } from "@/services/circle";
import type { import type {
@ -11,15 +11,13 @@ import type {
} from "@/types"; } from "@/types";
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) { function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return _.orderBy(completionData, ["updated_at"], "desc").find( return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
(c: CourseCompletion) => { return (
return ( c.completion_status === "success" &&
c.completion_status === "success" && c.course === courseId &&
c.course === courseId && c.page_type === "learnpath.LearningContent"
c.page_type === "learnpath.LearningContent" );
); });
}
);
} }
export class LearningPath implements WagtailLearningPath { export class LearningPath implements WagtailLearningPath {

View File

@ -7,7 +7,9 @@ import type {
CompetenceProfilePage, CompetenceProfilePage,
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import _ from "lodash"; import cloneDeep from "lodash/cloneDeep";
import groupBy from "lodash/groupBy";
import orderBy from "lodash/orderBy";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CompetenceStoreState = { export type CompetenceStoreState = {
@ -30,7 +32,7 @@ export const useCompetenceStore = defineStore({
actions: { actions: {
calcStatusCount(criteria: PerformanceCriteria[]) { calcStatusCount(criteria: PerformanceCriteria[]) {
if (criteria) { if (criteria) {
const grouped = _.groupBy(criteria, "completion_status"); const grouped = groupBy(criteria, "completion_status");
return { return {
fail: grouped?.fail?.length || 0, fail: grouped?.fail?.length || 0,
success: grouped?.success?.length || 0, success: grouped?.success?.length || 0,
@ -73,7 +75,7 @@ export const useCompetenceStore = defineStore({
if (this.competenceProfilePages.get(userId)) { if (this.competenceProfilePages.get(userId)) {
const competenceProfilePage = this.competenceProfilePages.get(userId); const competenceProfilePage = this.competenceProfilePages.get(userId);
if (competenceProfilePage) { if (competenceProfilePage) {
let criteria = _.orderBy( let criteria = orderBy(
competenceProfilePage.children.flatMap((competence) => { competenceProfilePage.children.flatMap((competence) => {
return competence.children; return competence.children;
}), }),
@ -147,7 +149,7 @@ export const useCompetenceStore = defineStore({
throw `No competenceProfilePageData found with: ${slug}`; throw `No competenceProfilePageData found with: ${slug}`;
} }
this.competenceProfilePages.set(userId, _.cloneDeep(competenceProfilePage)); this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage));
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}` };

View File

@ -6,7 +6,7 @@ import type {
CourseSessionUser, CourseSessionUser,
ExpertSessionUser, ExpertSessionUser,
} from "@/types"; } from "@/types";
import _ from "lodash"; import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
@ -72,7 +72,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
// these will become getters // these will become getters
const coursesFromCourseSessions = computed(() => const coursesFromCourseSessions = computed(() =>
// TODO: refactor after implementing of Klassenkonzept // TODO: refactor after implementing of Klassenkonzept
_.uniqBy(courseSessions.value, "course.id") uniqBy(courseSessions.value, "course.id")
); );
const courseSessionForRoute = computed(() => { const courseSessionForRoute = computed(() => {

View File

@ -2,7 +2,7 @@ import { itGetCached } from "@/fetchHelpers";
import { LearningPath } from "@/services/learningPath"; import { LearningPath } from "@/services/learningPath";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import _ from "lodash"; import cloneDeep from "lodash/cloneDeep";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type LearningPathStoreState = { export type LearningPathStoreState = {
@ -62,7 +62,7 @@ export const useLearningPathStore = defineStore({
); );
const learningPath = LearningPath.fromJson( const learningPath = LearningPath.fromJson(
_.cloneDeep(learningPathData), cloneDeep(learningPathData),
completionData completionData
); );
this.learningPaths.set(key, learningPath); this.learningPaths.set(key, learningPath);

View File

@ -112,9 +112,6 @@ def create_vv_new_learning_path(
lu_title="Selbsständigerwerbende versichern", lu_title="Selbsständigerwerbende versichern",
) )
TopicFactory(title="Vertiefen und Festigen", parent=lp)
create_circle_vernetzen(lp)
TopicFactory(title="Prüfung", parent=lp) TopicFactory(title="Prüfung", parent=lp)
create_circle_pruefungsvorbereitung(lp) create_circle_pruefungsvorbereitung(lp)
create_circle_pruefung(lp) create_circle_pruefung(lp)