Merge branch 'feature/course-permissions-frontend' into develop

This commit is contained in:
Daniel Egger 2022-11-11 18:43:21 +01:00
commit ca195c0b4a
17 changed files with 302 additions and 136 deletions

View File

@ -6,7 +6,7 @@ import IconSettings from "@/components/icons/IconSettings.vue";
import MobileMenu from "@/components/MobileMenu.vue"; import MobileMenu from "@/components/MobileMenu.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue"; import ItDropdown from "@/components/ui/ItDropdown.vue";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { useLearningPathStore } from "@/stores/learningPath"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { DropdownListItem } from "@/types"; import type { DropdownListItem } from "@/types";
import type { Component } from "vue"; import type { Component } from "vue";
@ -26,8 +26,9 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
const courseSessionsStore = useCourseSessionsStore();
const { t } = useI18n(); const { t } = useI18n();
const learningPathStore = useLearningPathStore();
const state = reactive({ showMenu: false }); const state = reactive({ showMenu: false });
function toggleNav() { function toggleNav() {
@ -35,6 +36,7 @@ function toggleNav() {
} }
function isInRoutePath(checkPaths: string[]) { function isInRoutePath(checkPaths: string[]) {
log.debug("isInRoutePath", checkPaths, route.path);
return checkPaths.some((path) => route.path.startsWith(path)); return checkPaths.some((path) => route.path.startsWith(path));
} }
@ -42,20 +44,6 @@ function inCourse() {
return isInRoutePath(["/learn", "/competence"]); return isInRoutePath(["/learn", "/competence"]);
} }
function getLearningPathStringProp(prop: "title" | "slug"): string {
return inCourse() && learningPathStore.learningPath
? learningPathStore.learningPath[prop]
: "";
}
function learningPathName(): string {
return getLearningPathStringProp("title");
}
function learninPathSlug(): string {
return getLearningPathStringProp("slug");
}
function handleDropdownSelect(data: DropdownData) { function handleDropdownSelect(data: DropdownData) {
switch (data.action) { switch (data.action) {
case "settings": case "settings":
@ -75,6 +63,9 @@ function logout() {
onMounted(() => { onMounted(() => {
log.debug("MainNavigationBar mounted"); log.debug("MainNavigationBar mounted");
if (userStore.loggedIn) {
courseSessionsStore.loadCourseSessionsData();
}
}); });
const profileDropdownData: DropdownListItem[] = [ const profileDropdownData: DropdownListItem[] = [
@ -100,8 +91,7 @@ const profileDropdownData: DropdownListItem[] = [
<Teleport to="body"> <Teleport to="body">
<MobileMenu <MobileMenu
:show="state.showMenu" :show="state.showMenu"
:learning-path-slug="learninPathSlug()" :course-session="courseSessionsStore.courseSessionForRoute"
:learning-path-name="learningPathName()"
@closemodal="state.showMenu = false" @closemodal="state.showMenu = false"
/> />
</Teleport> </Teleport>
@ -156,8 +146,8 @@ const profileDropdownData: DropdownListItem[] = [
class="flex-auto mt-8 lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0" class="flex-auto mt-8 lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0"
> >
<router-link <router-link
v-if="inCourse()" v-if="inCourse() && courseSessionsStore.courseSessionForRoute"
to="/learn/versicherungsvermittlerin-lp" :to="courseSessionsStore.courseSessionForRoute.learning_path_url"
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': isInRoutePath(['/learn']) }" :class="{ 'nav-item--active': isInRoutePath(['/learn']) }"
> >
@ -165,8 +155,8 @@ const profileDropdownData: DropdownListItem[] = [
</router-link> </router-link>
<router-link <router-link
v-if="inCourse()" v-if="inCourse() && courseSessionsStore.courseSessionForRoute"
to="/competence/versicherungsvermittlerin-competence" :to="courseSessionsStore.courseSessionForRoute.competence_url"
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': isInRoutePath(['/competence']) }" :class="{ 'nav-item--active': isInRoutePath(['/competence']) }"
> >

View File

@ -3,6 +3,7 @@ import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue"; import IconSettings from "@/components/icons/IconSettings.vue";
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
@ -10,15 +11,16 @@ const userStore = useUserStore();
const props = defineProps<{ const props = defineProps<{
show: boolean; show: boolean;
learningPathName: string; courseSession: CourseSession | undefined;
learningPathSlug: string;
}>(); }>();
const emits = defineEmits(["closemodal"]); const emits = defineEmits(["closemodal"]);
const clickLink = (to: string) => { const clickLink = (to: string | undefined) => {
router.push(to); if (to) {
emits("closemodal"); router.push(to);
emits("closemodal");
}
}; };
</script> </script>
@ -48,18 +50,16 @@ const clickLink = (to: string) => {
</div> </div>
</div> </div>
<div> <div>
<div v-if="true" class="mt-6 pb-6 border-b"> <div v-if="courseSession" class="mt-6 pb-6 border-b">
<h4 class="text-gray-900 text-sm">Kurs: Versicherungsvermittler/in</h4> <h4 class="text-gray-900 text-sm">{{ courseSession.course.title }}</h4>
<ul class="mt-6"> <ul class="mt-6">
<li> <li>
<button @click="clickLink(`/learn/versicherungsvermittlerin-lp`)"> <button @click="clickLink(courseSession?.learning_path_url)">
{{ $t("general.learningPath") }} {{ $t("general.learningPath") }}
</button> </button>
</li> </li>
<li class="mt-6"> <li class="mt-6">
<button <button @click="clickLink(courseSession?.competence_url)">
@click="clickLink(`/competence/versicherungsvermittlerin-competence`)"
>
KompetenzNavi KompetenzNavi
</button> </button>
</li> </li>

View File

@ -1,24 +1,21 @@
<script> <script>
import colors from "@/colors.json"; import colors from "@/colors.json";
import { useLearningPathStore } from "@/stores/learningPath";
import * as d3 from "d3"; import * as d3 from "d3";
import * as _ from "lodash"; import * as _ from "lodash";
import * as log from "loglevel"; import * as log from "loglevel";
// type DiagramType = "horizontal" | "vertical" | "horizontalSmall";
export default { export default {
props: { props: {
vertical: { diagramType: {
default: false,
type: Boolean,
},
identifier: {
required: true,
type: String, type: String,
default: "horizontal",
},
learningPath: {
required: true,
type: Object,
}, },
},
setup() {
const learningPathStore = useLearningPathStore();
return { learningPathStore };
}, },
data() { data() {
return { return {
@ -27,6 +24,9 @@ export default {
}; };
}, },
computed: { computed: {
svgId() {
return `learningpath-diagram-${this.learningPath.slug}-${this.diagramType}`;
},
viewBox() { viewBox() {
return `0 0 ${this.width} ${this.height}`; return `0 0 ${this.width} ${this.height}`;
}, },
@ -47,9 +47,9 @@ export default {
return false; return false;
} }
if (this.learningPathStore.learningPath) { if (this.learningPath) {
const internalCircles = []; const internalCircles = [];
this.learningPathStore.learningPath.circles.forEach((circle) => { this.learningPath.circles.forEach((circle) => {
const pieWeights = new Array( const pieWeights = new Array(
Math.max(circle.learningSequences.length, 1) Math.max(circle.learningSequences.length, 1)
).fill(1); ).fill(1);
@ -78,21 +78,27 @@ export default {
return []; return [];
}, },
svg() { svg() {
return d3.select("#" + this.identifier); const result = d3.select("#" + this.svgId);
}, result.selectAll("*").remove();
return result;
learningPath() {
return Object.assign({}, this.learningPathStore.learningPath);
}, },
}, },
mounted() { mounted() {
log.debug("LearningPathDiagram mounted"); log.debug("LearningPathDiagram mounted");
const circleWidth = this.vertical ? 60 : 200; // clean old svg
d3.select("#" + this.svgId)
.selectAll("*")
.remove();
let circleWidth = 200;
if (this.diagramType === "vertical") {
circleWidth = 60;
}
const radius = (circleWidth * 0.8) / 2; const radius = (circleWidth * 0.8) / 2;
if (this.vertical) { if (this.diagramType === "vertical") {
this.width = Math.min(960, window.innerWidth - 32); this.width = Math.min(960, window.innerWidth - 32);
this.height = 860; this.height = 860;
} else { } else {
@ -131,7 +137,7 @@ export default {
.append("g") .append("g")
.attr("class", "circle") .attr("class", "circle")
.attr("data-cy", (d) => { .attr("data-cy", (d) => {
if (this.vertical) { if (this.diagramType === "vertical") {
return `circle-${d.slug}-vertical`; return `circle-${d.slug}-vertical`;
} else { } else {
return `circle-${d.slug}`; return `circle-${d.slug}`;
@ -195,7 +201,7 @@ export default {
.style("font-size", "18px") .style("font-size", "18px")
.style("overflow-wrap", "break-word") .style("overflow-wrap", "break-word")
.text((d) => { .text((d) => {
if (!this.vertical) { if (this.diagramType === "horizontal") {
return d.title.replace("Prüfungsvorbereitung", "Prüfungs- vorbereitung"); return d.title.replace("Prüfungsvorbereitung", "Prüfungs- vorbereitung");
} }
return d.title; return d.title;
@ -266,7 +272,7 @@ export default {
// Calculate positions of objects // Calculate positions of objects
if (this.vertical) { if (this.diagramType === "vertical") {
const Circles_X = radius; const Circles_X = radius;
const Topics_X = Circles_X - 20; const Topics_X = Circles_X - 20;
@ -312,22 +318,28 @@ export default {
.attr("y", radius + 30) .attr("y", radius + 30)
.style("text-anchor", "middle") .style("text-anchor", "middle")
.call(wrap, circleWidth - 20) .call(wrap, circleWidth - 20)
.attr("class", "circlesText text-xl font-bold hidden lg:block"); .attr("class", () => {
let classes = "circlesText text-xl font-bold hidden";
if (this.diagramType === "horizontal") {
classes += " lg:block";
}
return classes;
});
topicGroups topicGroups
.attr("transform", (d, i) => { .attr("transform", (d, i) => {
return ( return (
"translate(" + "translate(" +
getTopicHorizontalPosition( getTopicHorizontalPosition(i, d, this.learningPath.topics) +
i,
d,
this.learningPathStore.learningPath.topics
) +
",0)" ",0)"
); );
}) })
.attr("class", (d) => { .attr("class", (d) => {
return "topic ".concat(d.is_visible ? "hidden lg:block" : "hidden"); let classes = "topic hidden";
if (this.diagramType === "horizontal" && d.is_visible) {
classes += " lg:block";
}
return classes;
}); });
topicLines topicLines
@ -343,7 +355,7 @@ export default {
.attr("y", 20) .attr("y", 20)
.style("font-size", "18px") .style("font-size", "18px")
.call(wrap, circleWidth * 0.8) .call(wrap, circleWidth * 0.8)
.attr("class", "topicTitles font-bold"); .attr("class", "topic-title font-bold");
} }
function wrap(text, width) { function wrap(text, width) {
@ -387,10 +399,11 @@ export default {
<template> <template>
<div class="svg-container h-full content-start"> <div class="svg-container h-full content-start">
<svg <svg
:id="identifier" :id="svgId"
class="learning-path-visualization h-full mx-auto -mt-6 lg:mt-0" class="learning-path-visualization h-full mx-auto -mt-6 lg:mt-0"
:class="{ :class="{
'max-h-[384px]': !vertical, 'max-h-[90px]': ['horizontalSmall'].includes(diagramType),
'max-h-[90px] lg:max-h-[380px]': ['horizontal'].includes(diagramType),
}" }"
:viewBox="viewBox" :viewBox="viewBox"
></svg> ></svg>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useLearningPathStore } from "@/stores/learningPath";
import * as log from "loglevel";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import type { LearningPath } from "@/services/learningPath";
import { ref } from "vue";
log.debug("LearningPathDiagramSmall created");
const props = defineProps<{
learningPathUrl: string;
}>();
const learningPathData = ref<LearningPath | undefined>(undefined);
const learningPathStore = useLearningPathStore();
learningPathStore
.loadLearningPath(props.learningPathUrl.replace("/learn/", ""), false, false)
.then((data) => {
learningPathData.value = data;
});
</script>
<template>
<LearningPathDiagram
v-if="learningPathData"
:learning-path="learningPathData"
diagram-type="horizontalSmall"
></LearningPathDiagram>
</template>
<style scoped></style>

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import * as log from "loglevel"; import * as log from "loglevel";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue"; import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
@ -14,7 +13,6 @@ const props = defineProps<{
}>(); }>();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const emits = defineEmits(["closemodal"]); const emits = defineEmits(["closemodal"]);
</script> </script>
@ -25,9 +23,10 @@ const emits = defineEmits(["closemodal"]);
<div class="learningpath flex flex-col"> <div class="learningpath flex flex-col">
<div class="flex flex-col h-max"> <div class="flex flex-col h-max">
<LearningPathDiagram <LearningPathDiagram
v-if="learningPathStore.learningPath"
class="w-full" class="w-full"
identifier="verticalVisualization" :learning-path="learningPathStore.learningPath"
:vertical="true" diagram-type="vertical"
></LearningPathDiagram> ></LearningPathDiagram>
</div> </div>
</div> </div>

View File

@ -6,6 +6,8 @@ import { computed } from "vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
const props = defineProps({ const props = defineProps({
// Vue-TypeScript make special things with `defineProps` so it's hard
// to make this without @ts-ignore...
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
...RouterLink.props, ...RouterLink.props,

View File

@ -56,3 +56,26 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
export const itGet = (url: RequestInfo) => { export const itGet = (url: RequestInfo) => {
return itPost(url, {}, { method: "GET" }); return itPost(url, {}, { method: "GET" });
}; };
const itGetPromiseCache = new Map<string, Promise<any>>();
export function bustItGetCache(key?: string) {
if (key) {
itGetPromiseCache.delete(key);
} else {
itGetPromiseCache.clear();
}
}
export const itGetCached = (
url: RequestInfo,
options = {
reload: false,
}
): Promise<any> => {
if (!itGetPromiseCache.has(url.toString()) || options.reload) {
itGetPromiseCache.set(url.toString(), itGet(url));
}
return itGetPromiseCache.get(url.toString()) as Promise<any>;
};

View File

@ -1,16 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathDiagramSmall from "@/components/learningPath/LearningPathDiagramSmall.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import * as log from "loglevel"; import log from "loglevel";
import { onMounted } from "vue";
log.debug("CockpitView created"); log.debug("CockpitView created");
const userStore = useUserStore(); const userStore = useUserStore();
const courseSessionsStore = useCourseSessionsStore();
function employer() { function employer() {
return userStore.email === "bianca.muster@eiger-versicherungen.ch" return userStore.email === "bianca.muster@eiger-versicherungen.ch"
? "Eiger Versicherungen" ? "Eiger Versicherungen"
: "VBV"; : "VBV";
} }
onMounted(async () => {
log.debug("CockpitView mounted");
await courseSessionsStore.loadCourseSessionsData();
});
</script> </script>
<template> <template>
@ -22,22 +31,26 @@ function employer() {
</h1> </h1>
<div class="mb-14"> <div class="mb-14">
<h2 class="mt-12 mb-3">Kurse</h2> <h2 class="mt-12 mb-3">Kurse</h2>
<div class="flex flex-col md:flex-row justify-between">
<div class="bg-white p-6 md:w-[48%] mb-4 md:mb-0"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 auto-rows-fr">
<h3 class="mb-4">Versicherungsvermittler/in</h3> <div
<img class="mb-8 block" :src="'/static/icons/demo/vm-lernpfad.svg'" /> v-for="courseSession in courseSessionsStore.courseSessions"
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp"> :key="courseSession.id"
{{ $t("general.nextStep") }} >
</router-link> <div class="bg-white p-6 md:h-full">
</div> <h3 class="mb-4">{{ courseSession.title }}</h3>
<div class="bg-white p-6 md:w-[48%]"> <div>
<h3 class="mb-4">Überbetriebliche Kurse</h3> <LearningPathDiagramSmall
<img class="mb-8 block" :src="'/static/icons/demo/uk-lernpfad.svg'" /> class="mb-4"
<button :learning-path-url="courseSession.learning_path_url"
class="bg-green-500 font-semibold py-2 px-4 align-middle inline-block text-blue-900 border-2 border-green-500" ></LearningPathDiagramSmall>
> </div>
Abgeschlossen <div>
</button> <router-link class="btn-blue" :to="courseSession.learning_path_url">
{{ $t("general.nextStep") }}
</router-link>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -68,9 +68,9 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
</button> </button>
</div> </div>
<LearningPathDiagram <LearningPathDiagram
class="mx-auto max-w-[1920px] max-h-[380px] w-full px-4" class="mx-auto max-w-[1920px] max-h-[90px] lg:max-h-[380px] w-full px-4"
identifier="mainVisualization" diagram-type="horizontal"
:vertical="false" :learning-path="learningPathStore.learningPath"
></LearningPathDiagram> ></LearningPathDiagram>
</div> </div>

View File

@ -1,7 +1,7 @@
import { itGet } from "@/fetchHelpers"; import { itGetCached } from "@/fetchHelpers";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import type { import type {
BaseCourseWagtailPage, CircleLight,
CompetencePage, CompetencePage,
CompetenceProfilePage, CompetenceProfilePage,
PerformanceCriteria, PerformanceCriteria,
@ -91,23 +91,17 @@ export const useCompetenceStore = defineStore({
}; };
}, },
async loadCompetenceProfilePage(slug: string, reload = false) { async loadCompetenceProfilePage(slug: string, reload = false) {
if (this.competenceProfilePage && !reload) { this.competenceProfilePage = await itGetCached(`/api/course/page/${slug}/`, {
await this.parseCompletionData(); reload: reload,
return this.competenceProfilePage; });
}
const competenceProfilePageData = await itGet(`/api/course/page/${slug}/`);
if (!competenceProfilePageData) { if (!this.competenceProfilePage) {
throw `No competenceProfilePageData found with: ${slug}`; throw `No competenceProfilePageData found with: ${slug}`;
} }
this.competenceProfilePage = competenceProfilePageData; const circles = this.competenceProfilePage.circles.map((c: CircleLight) => {
return { id: c.translation_key, name: `Circle: ${c.title}` };
const circles = competenceProfilePageData.circles.map( });
(c: BaseCourseWagtailPage) => {
return { id: c.translation_key, name: `Circle: ${c.title}` };
}
);
this.availableCircles = [{ id: "all", name: "Circle: Alle" }, ...circles]; this.availableCircles = [{ id: "all", name: "Circle: Alle" }, ...circles];
await this.parseCompletionData(); await this.parseCompletionData();

View File

@ -1,4 +1,4 @@
import { itGet, itPost } from "@/fetchHelpers"; import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import type { BaseCourseWagtailPage, CourseCompletion } from "@/types"; import type { BaseCourseWagtailPage, CourseCompletion } from "@/types";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -16,16 +16,14 @@ export const useCompletionStore = defineStore({
getters: {}, getters: {},
actions: { actions: {
async loadCompletionData(courseId: number, reload = false) { async loadCompletionData(courseId: number, reload = false) {
if (this.completionData && !reload) { this.completionData = await itGetCached(`/api/course/completion/${courseId}/`, {
return this.completionData; reload: reload,
} });
const completionData = await itGet(`/api/course/completion/${courseId}/`);
if (!completionData) { if (!this.completionData) {
throw `No completionData found with: ${courseId}`; throw `No completionData found with: ${courseId}`;
} }
this.completionData = completionData;
return this.completionData || []; return this.completionData || [];
}, },
async markPage(page: BaseCourseWagtailPage) { async markPage(page: BaseCourseWagtailPage) {
@ -34,8 +32,9 @@ export const useCompletionStore = defineStore({
completion_status: page.completion_status, completion_status: page.completion_status,
}); });
if (completionData) { if (completionData && completionData.length > 0) {
this.completionData = completionData; this.completionData = completionData;
bustItGetCache(`/api/course/completion/${completionData[0].course}/`);
} }
return this.completionData || []; return this.completionData || [];

View File

@ -0,0 +1,41 @@
import { itGetCached } from "@/fetchHelpers";
import type { CourseSession } from "@/types";
import log from "loglevel";
import { defineStore } from "pinia";
import { useRoute } from "vue-router";
export type CourseSessionsStoreState = {
courseSessions: CourseSession[] | undefined;
};
export const useCourseSessionsStore = defineStore({
id: "courseSessions",
state: () => {
return {
courseSessions: undefined,
} as CourseSessionsStoreState;
},
getters: {
courseSessionForRoute: (state) => {
const route = useRoute();
return state.courseSessions?.find((cs) => {
return (
route.path.startsWith(cs.learning_path_url) ||
route.path.startsWith(cs.competence_url)
);
});
},
},
actions: {
async loadCourseSessionsData(reload = false) {
log.debug("loadCourseSessionsData called");
this.courseSessions = await itGetCached(`/api/course/sessions/`, {
reload: reload,
});
if (!this.courseSessions) {
throw `No courseSessionData found for user`;
}
return this.courseSessions;
},
},
});

View File

@ -1,4 +1,4 @@
import { itGet } from "@/fetchHelpers"; 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 { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -6,34 +6,45 @@ import { defineStore } from "pinia";
export type LearningPathStoreState = { export type LearningPathStoreState = {
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
page: "INDEX" | "OVERVIEW"; page: "INDEX" | "OVERVIEW";
loading: boolean;
}; };
let lastSlug = "";
export const useLearningPathStore = defineStore({ export const useLearningPathStore = defineStore({
id: "learningPath", id: "learningPath",
state: () => { state: () => {
return { return {
learningPath: undefined, learningPath: undefined,
page: "INDEX", page: "INDEX",
loading: false,
} as LearningPathStoreState; } as LearningPathStoreState;
}, },
getters: {}, getters: {},
actions: { actions: {
async loadLearningPath(slug: string, reload = false) { async loadLearningPath(slug: string, reload = false, fail = true) {
this.loading = true;
const completionStore = useCompletionStore(); const completionStore = useCompletionStore();
if (this.learningPath && !reload) { if (this.learningPath && !reload && slug === lastSlug) {
return this.learningPath; return this.learningPath;
} }
const learningPathData = await itGet(`/api/course/page/${slug}/`); this.learningPath = undefined;
const completionData = await completionStore.loadCompletionData( const learningPathData = await itGetCached(`/api/course/page/${slug}/`);
learningPathData.course.id
);
if (!learningPathData) { if (!learningPathData && fail) {
throw `No learning path found with: ${slug}`; throw `No learning path found with: ${slug}`;
} }
this.learningPath = LearningPath.fromJson(learningPathData, completionData); if (learningPathData) {
return this.learningPath; lastSlug = slug;
const completionData = await completionStore.loadCompletionData(
learningPathData.course.id
);
this.learningPath = LearningPath.fromJson(learningPathData, completionData);
this.loading = false;
return this.learningPath;
}
}, },
}, },
}); });

View File

@ -1,7 +1,8 @@
import * as log from "loglevel"; import * as log from "loglevel";
import { itGet, itPost } from "@/fetchHelpers"; import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT; const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT;
@ -43,6 +44,8 @@ export const useUserStore = defineStore({
.then((data) => { .then((data) => {
this.$state = data; this.$state = data;
this.loggedIn = true; this.loggedIn = true;
log.debug("bust cache");
bustItGetCache();
log.debug(`redirect to ${next}`); log.debug(`redirect to ${next}`);
window.location.href = next; window.location.href = next;
}) })
@ -68,11 +71,13 @@ export const useUserStore = defineStore({
}, },
fetchUser() { fetchUser() {
const appStore = useAppStore(); const appStore = useAppStore();
itGet("/api/core/me/") itGetCached("/api/core/me/")
.then((data) => { .then((data) => {
this.$state = data; this.$state = data;
this.loggedIn = true; this.loggedIn = true;
appStore.userLoaded = true; appStore.userLoaded = true;
const courseSessionsStore = useCourseSessionsStore();
courseSessionsStore.loadCourseSessionsData();
}) })
.catch(() => { .catch(() => {
this.loggedIn = false; this.loggedIn = false;

View File

@ -301,3 +301,28 @@ export interface DropdownListItem {
icon: Component; icon: Component;
data: object; data: object;
} }
export interface CircleExpert {
user_id: number;
user_email: string;
user_first_name: string;
user_last_name: string;
circle_id: number;
circle_slug: string;
circle_translation_key: string;
}
export interface CourseSession {
id: number;
created_at: string;
updated_at: string;
course: Course;
title: string;
start_date: string;
end_date: string;
learning_path_url: string;
competence_url: string;
media_library_url: string;
additional_json_data: unknown;
experts: CircleExpert[];
}

View File

@ -107,26 +107,36 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
_create_student_user(**user_data) _create_student_user(**user_data)
_create_student_user( _create_student_user(
email="expertvv.analyse@vbv-afa.ch", email="expert-vv.analyse@eiger-versicherungen.ch",
first_name="Expert", first_name="Expert",
last_name="Analyse", last_name="Analyse",
) )
_create_student_user( _create_student_user(
email="expertvv.einstieg@vbv-afa.ch", email="expert-vv.einstieg@eiger-versicherungen.ch",
first_name="Expert", first_name="Expert",
last_name="Einstieg", last_name="Einstieg",
) )
_create_student_user(
email="student-vv@eiger-versicherungen.ch",
first_name="Student",
last_name="VV",
)
_create_student_user( _create_student_user(
email="trainer-uk1.analyse@vbv-afa.ch", email="trainer-uk1-bern.analyse@eiger-versicherungen.ch",
first_name="Trainer", first_name="Trainer",
last_name="Analyse", last_name="Analyse",
) )
_create_student_user( _create_student_user(
email="trainer-uk1.einstieg@vbv-afa.ch", email="trainer-uk1-bern.einstieg@eiger-versicherungen.ch",
first_name="Trainer", first_name="Trainer",
last_name="Einstieg", last_name="Einstieg",
) )
_create_student_user(
email="student-uk1-bern@eiger-versicherungen.ch",
first_name="Student",
last_name="UK1-Bern",
)
def _get_or_create_user(user_model, *args, **kwargs): def _get_or_create_user(user_model, *args, **kwargs):

View File

@ -70,15 +70,18 @@ def command():
) )
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
user=User.objects.get(username="expertvv.einstieg@vbv-afa.ch"), user=User.objects.get(username="student-vv@eiger-versicherungen.ch"),
)
csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(username="expert-vv.einstieg@eiger-versicherungen.ch"),
) )
csu.expert.add( csu.expert.add(
Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-einstieg") Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-einstieg")
) )
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
user=User.objects.get(username="expertvv.analyse@vbv-afa.ch"), user=User.objects.get(username="expert-vv.analyse@eiger-versicherungen.ch"),
) )
csu.expert.add( csu.expert.add(
Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-analyse") Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-analyse")
@ -94,15 +97,21 @@ def command():
course_session=cs, course_session=cs,
user=User.objects.get(username=user_data["email"]), user=User.objects.get(username=user_data["email"]),
) )
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
user=User.objects.get(username="trainer-uk1.einstieg@vbv-afa.ch"), user=User.objects.get(
username="trainer-uk1-bern.einstieg@eiger-versicherungen.ch"
),
) )
csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-einstieg")) csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-einstieg"))
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
user=User.objects.get(username="trainer-uk1.analyse@vbv-afa.ch"), user=User.objects.get(
username="trainer-uk1-bern.analyse@eiger-versicherungen.ch"
),
) )
csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-analyse")) csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-analyse"))
csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(username="student-uk1-bern@eiger-versicherungen.ch"),
)