Add circle view for user profiles in cockpit

This commit is contained in:
Daniel Egger 2022-12-14 19:02:48 +01:00
parent 38114b55c5
commit e8073753be
21 changed files with 190 additions and 64 deletions

View File

@ -3,5 +3,6 @@
"singleQuote": false,
"tabWidth": 2,
"printWidth": 88,
"organizeImportsSkipDestructiveCodeActions": true
"organizeImportsSkipDestructiveCodeActions": true,
"htmlWhitespaceSensitivity": "ignore"
}

View File

@ -204,8 +204,9 @@ const profileDropdownData: DropdownListItem[] = [
class="nav-item"
target="_blank"
href="https://bildung.vbv.ch/ilp/pages/catalogsearch.jsf"
>Shop</a
>
Shop
</a>
<router-link
to="/media/versicherungsvermittlerin-media"
class="nav-item"

View File

@ -44,7 +44,7 @@ const clickLink = (to: string | undefined) => {
@click="clickLink('/profile')"
>
<IconSettings class="inline-block" />
<span class="ml-3"> {{ $t("mainNavigation.settings") }} </span>
<span class="ml-3">{{ $t("mainNavigation.settings") }}</span>
</button>
</div>
</div>
@ -72,8 +72,9 @@ const clickLink = (to: string | undefined) => {
class="nav-item"
target="_blank"
href="https://bildung.vbv.ch/ilp/pages/catalogsearch.jsf"
>Shop</a
>
Shop
</a>
</li>
<li class="mt-6">
<button @click="clickLink(`/media/versicherungsvermittlerin-media`)">

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import type { DefaultArcObject } from "d3";
import * as d3 from "d3";
import * as _ from "lodash";
import * as log from "loglevel";
@ -8,7 +9,6 @@ import { computed, onMounted } from "vue";
// @ts-ignore
import colors from "@/colors.json";
import type { LearningSequence } from "@/types";
import type { DefaultArcObject } from "d3";
const circleStore = useCircleStore();
@ -48,11 +48,8 @@ interface CirclePie extends d3.PieArcDatum<number> {
const pieData = computed(() => {
const circle = circleStore.circle;
console.log("initial of compute pie data ", circle);
if (circle) {
console.log("initial of compute pie data ", circle);
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
const pieGenerator = d3.pie();
const angles = pieGenerator(pieWeights);
@ -177,7 +174,7 @@ function render() {
});
})
.on("click", function (d, elm) {
console.log("clicked on ", d, elm);
log.info("clicked on ", d, elm);
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
});

View File

@ -55,8 +55,7 @@ const block = computed(() => {
frameborder="0"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
>
</iframe>
></iframe>
</div>
<div v-else-if="block.type === 'media_library'" class="mt-4 lg:mt-12">

View File

@ -17,14 +17,18 @@ export type DiagramType = "horizontal" | "vertical" | "horizontalSmall";
export interface Props {
diagramType?: DiagramType;
postfix?: string;
profileUserId?: string;
learningPath: LearningPath;
}
const props = withDefaults(defineProps<Props>(), {
diagramType: "horizontal",
postfix: "",
profileUserId: "",
});
log.debug("LearningPathDiagram created", props.postfix, props.profileUserId);
const state = reactive({ width: 1640, height: 384 });
const svgId = computed(() => {
@ -38,8 +42,7 @@ const viewBox = computed(() => {
const vueRouter = useRouter();
onMounted(async () => {
log.debug("LearningPathDiagram mounted");
console.log(props.learningPath);
log.debug("LearningPathDiagram mounted", props.postfix, props.profileUserId);
render();
});
@ -57,6 +60,14 @@ function allFinished(circle: Circle, learningSequence: LearningSequence) {
return false;
}
function circleUrl(circle: InternalCircle) {
let circleUrl = circle.frontend_url;
if (props.profileUserId) {
circleUrl = `/course/${props.learningPath.course.slug}/cockpit/profile/${props.profileUserId}/${circle.slug}`;
}
return circleUrl;
}
interface CirclePie extends d3.PieArcDatum<number> {
someFinished: boolean;
allFinished: boolean;
@ -90,6 +101,7 @@ const circles = computed(() => {
pie.someFinished = someFinished(circle, thisLearningSequence);
pie.allFinished = allFinished(circle, thisLearningSequence);
});
internalCircles.push({
pieData: pieData.reverse() as CirclePie[],
title: circle.title,
@ -181,7 +193,7 @@ function render() {
});
})
.on("click", (d, i) => {
vueRouter.push(i.frontend_url);
vueRouter.push(circleUrl(i));
})
.attr("role", "button");

View File

@ -11,9 +11,14 @@ import _ from "lodash";
import { computed } from "vue";
import LearningContentBadge from "./LearningContentTypeBadge.vue";
const props = defineProps<{
interface Props {
learningSequence: LearningSequence;
}>();
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false,
});
const circleStore = useCircleStore();
@ -112,14 +117,26 @@ const learningSequenceBorderClass = computed(() => {
:key="learningContent.id"
class="flex gap-4 pb-3 lg:pb-6 items-center"
>
<div v-if="props.readonly">
<it-icon-check
v-if="learningContent.completion_status === 'success'"
class="block w-8 h-8"
></it-icon-check>
<div v-else class="w-8 h-8"></div>
</div>
<ItCheckbox
v-else
:model-value="learningContent.completion_status === 'success'"
:on-toggle="() => toggleCompleted(learningContent)"
:data-cy="`${learningContent.slug}-checkbox`"
/>
<div class="flex-auto pt-1 sm:pt-0">
<span class="flex gap-4 items-center xl:h-10">
<div v-if="props.readonly" class="w-full sm:w-auto text-left">
{{ learningContent.title }}
</div>
<button
v-else
class="cursor-pointer w-full sm:w-auto text-left"
:data-cy="`${learningContent.slug}`"
@click.stop="circleStore.openLearningContent(learningContent)"
@ -132,7 +149,8 @@ const learningSequenceBorderClass = computed(() => {
>
<button
v-if="
learningContent.translation_key === continueTranslationKeyTuple[0]
learningContent.translation_key ===
continueTranslationKeyTuple[0] && !props.readonly
"
class="btn-blue order-1 sm:order-none"
data-cy="ls-continue-button"
@ -162,9 +180,9 @@ const learningSequenceBorderClass = computed(() => {
<div
v-if="learningUnit.children.length"
class="hover:cursor-pointer"
:class="{ 'cursor-pointer': !props.readonly }"
:data-cy="`${learningUnit.slug}`"
@click="circleStore.openSelfEvaluation(learningUnit)"
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
>
<div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'success'"

View File

@ -65,7 +65,8 @@ function handleBack() {
<p class="text-large mt-4">
{{ $t("selfEvaluation.instruction[0]") }}
<span class="font-bold">«{{ learningUnit.title }}»</span>
{{ $t("selfEvaluation.instruction[1]") }}<br />
{{ $t("selfEvaluation.instruction[1]") }}
<br />
{{ $t("selfEvaluation.instruction[2]") }}
</p>
@ -85,9 +86,7 @@ function handleBack() {
@click="circleStore.markCompletion(currentQuestion, 'success')"
>
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-large">
{{ $t("selfEvaluation.yes") }}.
</span>
<span class="font-bold text-large">{{ $t("selfEvaluation.yes") }}.</span>
</button>
<button
class="flex-1 inline-flex items-center text-left p-4 border"
@ -99,7 +98,7 @@ function handleBack() {
@click="circleStore.markCompletion(currentQuestion, 'fail')"
>
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>
<span class="font-bold text-xl"> {{ $t("selfEvaluation.no") }}. </span>
<span class="font-bold text-xl">{{ $t("selfEvaluation.no") }}.</span>
</button>
</div>

View File

@ -32,7 +32,7 @@ function setIsOpen(value: boolean) {
<it-icon-close></it-icon-close>
</button>
</DialogTitle>
<DialogDescription> </DialogDescription>
<DialogDescription></DialogDescription>
<slot name="body"></slot>
</DialogPanel>

View File

@ -112,7 +112,8 @@ onMounted(async () => {
<p class="text-bold">Check-up</p>
<p>Vermittler/-in VBV</p>
<p>
Gültig bis: <span class="text-green-500 text-bold">31.12.2026</span>
Gültig bis:
<span class="text-green-500 text-bold">31.12.2026</span>
</p>
</div>
</div>
@ -122,7 +123,10 @@ onMounted(async () => {
<div>
<p class="text-bold">Zulassungsprüfung</p>
<p>Vermittler/-in VBV</p>
<p>Bestanden: <span class="text-bold">14.11.2022</span></p>
<p>
Bestanden:
<span class="text-bold">14.11.2022</span>
</p>
</div>
</div>
</div>

View File

@ -356,14 +356,13 @@ function log(data: any) {
v-model="state.dropdownSelected"
class="w-full lg:w-96 mt-4 lg:mt-0"
:items="state.dropdownValues"
>
</ItDropdownSelect>
></ItDropdownSelect>
{{ state.dropdownSelected }}
<h2 class="mt-8 mb-8">Checkbox</h2>
<ItCheckbox v-model="state.checkboxValue" :disabled="false" class=""
>Label
<ItCheckbox v-model="state.checkboxValue" :disabled="false" class="">
Label
</ItCheckbox>
<ItCheckbox disabled class="mt-4">Disabled</ItCheckbox>
@ -376,7 +375,8 @@ function log(data: any) {
:list-items="dropdownData"
:align="'left'"
@select="log"
>Click Me
>
Click Me
</ItDropdown>
</div>
</main>

View File

@ -8,7 +8,6 @@ import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath";
import * as log from "loglevel";
import { ref } from "vue";
const props = defineProps<{
courseSlug: string;
@ -127,6 +126,7 @@ function setActiveClasses(id: number) {
) as LearningPath
"
:postfix="`cockpit-${csu.user_id}`"
:profile-user-id="`${csu.user_id}`"
diagram-type="horizontalSmall"
></LearningPathDiagram>
</div>
@ -134,8 +134,9 @@ function setActiveClasses(id: number) {
<span
v-for="title in cockpitStore.selectedCirclesTitles"
:key="title"
>{{ title }}</span
>
{{ title }}
</span>
</div>
</div>
<div class="ml-4 flex flex-row items-center">

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import CirclePage from "@/pages/learningPath/CirclePage.vue";
import { useCockpitStore } from "@/stores/cockpit";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
const props = defineProps<{
userId: string;
courseSlug: string;
circleSlug: string;
}>();
log.debug("CockpitUserCirclePage created", props.userId, props.circleSlug);
const cockpitStore = useCockpitStore();
onMounted(async () => {
log.debug("CockpitUserCirclePage mounted");
});
const user = computed(() => {
return cockpitStore.courseSessionUsers?.find(
(csu) => csu.user_id === Number(props.userId)
);
});
</script>
<template>
<CirclePage
v-if="user"
:course-slug="props.courseSlug"
:circle-slug="props.circleSlug"
:profile-user="user"
:readonly="true"
></CirclePage>
</template>
<style scoped></style>

View File

@ -67,6 +67,7 @@ function setActiveClasses(isActive: boolean) {
diagram-type="horizontal"
:learning-path="learningPath"
:postfix="userId"
:profile-user-id="userId"
></LearningPathDiagram>
</div>
<ul class="flex flex-row border-b-2 mb-5">

View File

@ -80,7 +80,7 @@ findCriteria();
@click="circleStore.markCompletion(currentQuestion, 'success')"
>
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-large"> {{ $t("selfEvaluation.yes") }} </span>
<span class="font-bold text-large">{{ $t("selfEvaluation.yes") }}</span>
</button>
<button
class="flex-1 inline-flex items-center text-left p-4 border"
@ -92,7 +92,7 @@ findCriteria();
@click="circleStore.markCompletion(currentQuestion, 'fail')"
>
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>
<span class="font-bold text-xl"> {{ $t("selfEvaluation.no") }} </span>
<span class="font-bold text-xl">{{ $t("selfEvaluation.no") }}</span>
</button>
</div>
</div>

View File

@ -5,14 +5,14 @@ import LearningSequence from "@/components/learningPath/LearningSequence.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItModal from "@/components/ui/ItModal.vue";
import * as log from "loglevel";
import { reactive, ref } from "vue";
import { computed, onMounted, reactive, ref } from "vue";
import { useAppStore } from "@/stores/app";
import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSessionUser } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash";
import { computed, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
@ -20,12 +20,19 @@ const route = useRoute();
const { t } = useI18n();
const courseSessionsStore = useCourseSessionsStore();
log.debug("CircleView.vue created", route);
const props = defineProps<{
interface Props {
courseSlug: string;
circleSlug: string;
}>();
profileUser?: CourseSessionUser;
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false,
profileUser: undefined,
});
log.debug("CirclePage created", props.readonly, props.profileUser);
const showUploadModal = ref(false);
const formData = reactive({
@ -59,10 +66,23 @@ const dropdownLearningSequences = computed(() =>
);
onMounted(async () => {
log.debug("CircleView.vue mounted", props.courseSlug, props.circleSlug);
log.debug(
"CirclePage mounted",
props.courseSlug,
props.circleSlug,
props.profileUser
);
try {
if (props.profileUser) {
await circleStore.loadCircle(
props.courseSlug,
props.circleSlug,
props.profileUser.user_id
);
} else {
await circleStore.loadCircle(props.courseSlug, props.circleSlug);
}
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
const slugEnd = route.hash.replace("#", "");
@ -107,9 +127,33 @@ onMounted(async () => {
<div>
<div class="circle-container bg-gray-200">
<div class="circle max-w-9xl">
<div v-if="profileUser" class="user-profile">
<header
class="flex flex-row items-center p-8 bg-white relative shadow-xl"
>
<img
class="w-32 h-32 rounded-full mr-8"
:src="profileUser.avatar_url"
/>
<div>
<h1 class="mb-2">
{{ profileUser.first_name }} {{ profileUser.last_name }}
</h1>
<div>
<router-link
class="link"
:to="`/course/${courseSlug}/cockpit/profile/${profileUser.user_id}`"
>
Profil anzeigen
</router-link>
</div>
</div>
</header>
</div>
<div class="flex flex-col lg:flex-row">
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:pt-4 bg-white">
<router-link
v-if="!props.readonly"
:to="`/course/${props.courseSlug}/learn`"
class="btn-text inline-flex items-center px-3 py-4"
data-cy="back-to-learning-path-button"
@ -128,7 +172,7 @@ onMounted(async () => {
<CircleDiagram></CircleDiagram>
</div>
<div class="border-t-2 mt-4 lg:hidden">
<div v-if="!props.readonly" class="border-t-2 mt-4 lg:hidden">
<div
class="mt-4 inline-flex items-center"
@click="circleStore.page = 'OVERVIEW'"
@ -136,7 +180,7 @@ onMounted(async () => {
<it-icon-info class="mr-2" />
{{ $t("circlePage.circleContentBoxTitle") }}
</div>
<div class="inline-flex items-center">
<div v-if="!props.readonly" class="inline-flex items-center">
<it-icon-message class="mr-2" />
Fachexpertin kontaktieren
</div>
@ -159,7 +203,7 @@ onMounted(async () => {
</button>
</div>
<div class="block border mt-8 p-6">
<div v-if="!props.readonly" class="block border mt-8 p-6">
<h3 class="text-blue-dark">
{{ $t("circlePage.documents.title") }}
</h3>
@ -176,7 +220,7 @@ onMounted(async () => {
</div>
</div>
<div class="expert border mt-8 p-6">
<div v-if="!props.readonly" class="expert border mt-8 p-6">
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
<div class="leading-relaxed mt-4">
Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.
@ -196,6 +240,7 @@ onMounted(async () => {
>
<LearningSequence
:learning-sequence="learningSequence"
:readonly="props.readonly"
></LearningSequence>
</li>
</ol>
@ -206,31 +251,30 @@ onMounted(async () => {
<template #title>{{ $t("circlePage.documents.action") }}</template>
<template #body>
<form>
<label class="block text-bold" for="upload">{{
$t("circlePage.documents.fileLabel")
}}</label>
<label class="block text-bold" for="upload">
{{ $t("circlePage.documents.fileLabel") }}
</label>
<div class="btn-secondary mt-4 mb-8 text-xl relative cursor-pointer">
<input id="upload" type="file" class="absolute opacity-0" />
{{ $t("circlePage.documents.modalAction") }}
</div>
<!--p>{{ $t("circlePage.documentsModalInformation") }}</p-->
<div class="mb-8">
<label class="block text-bold mb-4" for="name">{{
$t("circlePage.documents.modalFileName")
}}</label>
<input type="text" id="name" class="w-1/2 mb-2" />
<label class="block text-bold mb-4" for="name">
{{ $t("circlePage.documents.modalFileName") }}
</label>
<input id="name" type="text" class="w-1/2 mb-2" />
<p>{{ $t("circlePage.documents.modalNameInformation") }}</p>
</div>
<div class="mb-8">
<label class="block text-bold mb-4" for="learningsequnce">{{
$t("general.learningSequence")
}}</label>
<label class="block text-bold mb-4" for="learningsequnce">
{{ $t("general.learningSequence") }}
</label>
<ItDropdownSelect
v-model="formData.learningSequence"
class="w-full lg:w-96 mt-4 lg:mt-0"
:items="dropdownLearningSequences"
>
</ItDropdownSelect>
></ItDropdownSelect>
</div>
<div class="-mx-8 px-8 pt-4 border-t">
<button class="btn-primary text-xl mb-0">

View File

@ -50,8 +50,7 @@ watch(dropdownSelected, (newValue) =>
:description="$t('mediaLibrary.learningMedia.description')"
icon="lernmedien-overview"
class="mb-6"
>
</OverviewCard>
></OverviewCard>
</div>
</template>

View File

@ -74,7 +74,8 @@ const mediaList = computed(() => {
:to="item.value.url"
:blank="item.value.open_window"
class="link"
>{{ item.value.link_display_text }}
>
{{ item.value.link_display_text }}
</media-link>
</div>
</li>

View File

@ -114,6 +114,11 @@ const router = createRouter({
component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"),
props: true,
},
{
path: "profile/:userId/:circleSlug",
component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"),
props: true,
},
],
},
{

View File

@ -227,6 +227,7 @@ class CourseSessionUser(models.Model):
def to_dict(self):
return {
"session_id": self.course_session.id,
"session_title": self.course_session.title,
"user_id": self.user.id,
"first_name": self.user.first_name,

View File

@ -138,7 +138,11 @@ def get_course_session_users(request, course_slug):
data = {
"cockpit_user": cockpit_user_csu[0].to_dict()
| {"circles": cockpit_user_csu[0].expert.all().values("id", "title")},
| {
"circles": cockpit_user_csu[0]
.expert.all()
.values("id", "title", "slug", "translation_key")
},
"users": user_data,
}