Add multiple rows in cockpit list circles
This commit is contained in:
parent
8b2aab8298
commit
bbd74d85b3
|
|
@ -2,7 +2,7 @@
|
|||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { DefaultArcObject } from "d3";
|
||||
import * as d3 from "d3";
|
||||
import * as _ from "lodash";
|
||||
import pick from "lodash/pick";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ const pieData = computed(() => {
|
|||
{
|
||||
startAngle: angle.startAngle,
|
||||
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,
|
||||
arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
|
||||
someFinished: someFinished(thisLearningSequence),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import * as d3 from "d3";
|
||||
import * as _ from "lodash";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import * as log from "loglevel";
|
||||
|
||||
// @ts-ignore
|
||||
|
|
@ -12,7 +12,7 @@ import type { LearningSequence, Topic } from "@/types";
|
|||
import { computed, onMounted, reactive, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export type DiagramType = "horizontal" | "vertical" | "horizontalSmall";
|
||||
export type DiagramType = "horizontal" | "vertical" | "horizontalSmall" | "singleSmall";
|
||||
|
||||
export interface Props {
|
||||
diagramType?: DiagramType;
|
||||
|
|
@ -21,6 +21,7 @@ export interface Props {
|
|||
learningPath: LearningPath;
|
||||
// set to undefined (default) to show all circles
|
||||
showCircleTranslationKeys: string[];
|
||||
pullUp?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -28,6 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
postfix: "",
|
||||
profileUserId: "",
|
||||
showCircleTranslationKeys: undefined,
|
||||
pullUp: true,
|
||||
});
|
||||
|
||||
log.debug(
|
||||
|
|
@ -37,14 +39,14 @@ log.debug(
|
|||
props.showCircleTranslationKeys
|
||||
);
|
||||
|
||||
const state = reactive({ width: 1640, height: 384 });
|
||||
const state = reactive({ width: 1640, height: 384, startY: 0 });
|
||||
|
||||
const svgId = computed(() => {
|
||||
return `learningpath-diagram-${props.learningPath?.slug}-${props.diagramType}${props.postfix}`;
|
||||
});
|
||||
|
||||
const viewBox = computed(() => {
|
||||
return `0 0 ${state.width} ${state.height}`;
|
||||
return `0 ${state.startY} ${state.width} ${state.height}`;
|
||||
});
|
||||
|
||||
const vueRouter = useRouter();
|
||||
|
|
@ -135,7 +137,7 @@ const circles = computed(() => {
|
|||
frontend_url: circle.frontend_url,
|
||||
id: circle.id,
|
||||
translation_key: circle.translation_key,
|
||||
slug: _.kebabCase(circle.title),
|
||||
slug: kebabCase(circle.title),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -164,6 +166,10 @@ function render() {
|
|||
if (props.diagramType === "vertical") {
|
||||
state.width = Math.min(960, window.innerWidth - 32);
|
||||
state.height = 860;
|
||||
} else if (props.diagramType === "singleSmall") {
|
||||
state.height = 260;
|
||||
state.startY = 60;
|
||||
state.width = circleWidth * circles.value.length;
|
||||
} else {
|
||||
state.width = circleWidth * circles.value.length;
|
||||
}
|
||||
|
|
@ -466,10 +472,11 @@ function render() {
|
|||
<div class="svg-container h-full content-start">
|
||||
<svg
|
||||
: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="{
|
||||
'max-h-[90px]': ['horizontalSmall'].includes(diagramType),
|
||||
'max-h-[90px]': ['horizontalSmall', 'singleSmall'].includes(diagramType),
|
||||
'max-h-[90px] lg:max-h-[380px]': ['horizontal'].includes(diagramType),
|
||||
'-mt-6': pullUp,
|
||||
}"
|
||||
:viewBox="viewBox"
|
||||
></svg>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
LearningSequence,
|
||||
} from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import _ from "lodash";
|
||||
import findLast from "lodash/findLast";
|
||||
import { computed } from "vue";
|
||||
import LearningContentBadge from "./LearningContentTypeBadge.vue";
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ const allFinished = computed(() => {
|
|||
|
||||
const continueTranslationKeyTuple = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
const lastFinished = _.findLast(
|
||||
const lastFinished = findLast(
|
||||
circleStore.circle.flatLearningContents,
|
||||
(learningContent) => {
|
||||
return learningContent.completion_status === "success";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<slot name="center"></slot>
|
||||
</div>
|
||||
<div class="flex items-center md:w-1/4">
|
||||
<div class="flex items-center lg:w-1/4">
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useCompetenceStore } from "@/stores/competence";
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
|
@ -25,10 +26,14 @@ const competenceStore = useCompetenceStore();
|
|||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
|
||||
function userCountStatus(userId: number) {
|
||||
return competenceStore.calcStatusCount(
|
||||
competenceStore.flatPerformanceCriteria(userId, cockpitStore.selectedCircles)
|
||||
function userCountStatusForCircle(userId: number, translationKey: string) {
|
||||
const criteria = competenceStore.flatPerformanceCriteria(
|
||||
userId,
|
||||
cockpitStore.selectedCircles
|
||||
);
|
||||
const grouped = groupBy(criteria, "circle.translation_key");
|
||||
|
||||
return competenceStore.calcStatusCount(grouped[translationKey]);
|
||||
}
|
||||
|
||||
const circles = computed(() => {
|
||||
|
|
@ -141,9 +146,15 @@ function setActiveClasses(translationKey: string) {
|
|||
:avatar-url="csu.avatar_url"
|
||||
>
|
||||
<template #center>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="mr-6 flex flex-row items-center gap-4">
|
||||
<div class="h-12">
|
||||
<div
|
||||
class="mt-2 flex w-full flex-row items-center justify-between lg:mt-0"
|
||||
>
|
||||
<ul class="w-full">
|
||||
<li
|
||||
class="flex h-12 items-center justify-between"
|
||||
v-for="(circle, i) of cockpitStore.selectedCircles"
|
||||
:key="i"
|
||||
>
|
||||
<LearningPathDiagram
|
||||
v-if="
|
||||
learningPathStore.learningPathForUser(
|
||||
|
|
@ -157,44 +168,44 @@ function setActiveClasses(translationKey: string) {
|
|||
csu.user_id
|
||||
) as LearningPath
|
||||
"
|
||||
:postfix="`cockpit-${csu.user_id}`"
|
||||
:postfix="`cockpit-${csu.user_id}-${i}`"
|
||||
:profile-user-id="`${csu.user_id}`"
|
||||
:show-circle-translation-keys="cockpitStore.selectedCircles"
|
||||
diagram-type="horizontalSmall"
|
||||
:show-circle-translation-keys="[circle]"
|
||||
:pull-up="false"
|
||||
diagram-type="singleSmall"
|
||||
class="mr-4"
|
||||
></LearningPathDiagram>
|
||||
</div>
|
||||
<div>
|
||||
<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 class="lg:min-w-[150px]">
|
||||
{{ selectedCirclesTitles[i] }}
|
||||
</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 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">
|
||||
{{ userCountStatus(csu.user_id).unknown }}
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<template #link>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import PerformanceCriteriaRow from "@/components/competences/PerformanceCriteria
|
|||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import _ from "lodash";
|
||||
import maxBy from "lodash/maxBy";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ const failedCriteria = computed(() => {
|
|||
});
|
||||
|
||||
const lastUpdatedCompetences = computed(() => {
|
||||
return _.orderBy(
|
||||
return orderBy(
|
||||
competenceStore.competences(),
|
||||
[
|
||||
(competence) => {
|
||||
|
|
@ -38,7 +39,7 @@ const lastUpdatedCompetences = computed(() => {
|
|||
});
|
||||
}
|
||||
return (
|
||||
_.maxBy(criteria, "completion_status_updated_at")
|
||||
maxBy(criteria, "completion_status_updated_at")
|
||||
?.completion_status_updated_at || ""
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { useCircleStore } from "@/stores/circle";
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { CourseSessionUser, DocumentUploadData } from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import _ from "lodash";
|
||||
import sumBy from "lodash/sumBy";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
|
@ -44,7 +44,7 @@ const circleStore = useCircleStore();
|
|||
|
||||
const duration = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
const minutes = _.sumBy(circleStore.circle.learningSequences, "minutes");
|
||||
const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
|
||||
return humanizeDuration(minutes);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import type {
|
|||
LearningUnitPerformanceCriteria,
|
||||
WagtailCircle,
|
||||
} from "@/types";
|
||||
import _ from "lodash";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import partition from "lodash/partition";
|
||||
import values from "lodash/values";
|
||||
|
||||
export function parseLearningSequences(
|
||||
circle: Circle,
|
||||
|
|
@ -192,7 +194,7 @@ export class Circle implements WagtailCircle {
|
|||
|
||||
public allFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
const [performanceCriteria, learningContents] = _.partition(
|
||||
const [performanceCriteria, learningContents] = partition(
|
||||
this.flatChildren.filter(
|
||||
(lc) => lc.parentLearningSequence?.translation_key === translationKey
|
||||
),
|
||||
|
|
@ -201,8 +203,8 @@ export class Circle implements WagtailCircle {
|
|||
}
|
||||
);
|
||||
|
||||
const groupedPerformanceCriteria = _.values(
|
||||
_.groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
|
||||
const groupedPerformanceCriteria = values(
|
||||
groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as _ from "lodash";
|
||||
import orderBy from "lodash/orderBy";
|
||||
|
||||
import { Circle } from "@/services/circle";
|
||||
import type {
|
||||
|
|
@ -11,15 +11,13 @@ import type {
|
|||
} from "@/types";
|
||||
|
||||
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
|
||||
return _.orderBy(completionData, ["updated_at"], "desc").find(
|
||||
(c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "success" &&
|
||||
c.course === courseId &&
|
||||
c.page_type === "learnpath.LearningContent"
|
||||
);
|
||||
}
|
||||
);
|
||||
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "success" &&
|
||||
c.course === courseId &&
|
||||
c.page_type === "learnpath.LearningContent"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export class LearningPath implements WagtailLearningPath {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import type {
|
|||
CompetenceProfilePage,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import _ from "lodash";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type CompetenceStoreState = {
|
||||
|
|
@ -30,7 +32,7 @@ export const useCompetenceStore = defineStore({
|
|||
actions: {
|
||||
calcStatusCount(criteria: PerformanceCriteria[]) {
|
||||
if (criteria) {
|
||||
const grouped = _.groupBy(criteria, "completion_status");
|
||||
const grouped = groupBy(criteria, "completion_status");
|
||||
return {
|
||||
fail: grouped?.fail?.length || 0,
|
||||
success: grouped?.success?.length || 0,
|
||||
|
|
@ -73,7 +75,7 @@ export const useCompetenceStore = defineStore({
|
|||
if (this.competenceProfilePages.get(userId)) {
|
||||
const competenceProfilePage = this.competenceProfilePages.get(userId);
|
||||
if (competenceProfilePage) {
|
||||
let criteria = _.orderBy(
|
||||
let criteria = orderBy(
|
||||
competenceProfilePage.children.flatMap((competence) => {
|
||||
return competence.children;
|
||||
}),
|
||||
|
|
@ -147,7 +149,7 @@ export const useCompetenceStore = defineStore({
|
|||
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) => {
|
||||
return { id: c.translation_key, name: `Circle: ${c.title}` };
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
CourseSessionUser,
|
||||
ExpertSessionUser,
|
||||
} from "@/types";
|
||||
import _ from "lodash";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
|
@ -72,7 +72,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
// these will become getters
|
||||
const coursesFromCourseSessions = computed(() =>
|
||||
// TODO: refactor after implementing of Klassenkonzept
|
||||
_.uniqBy(courseSessions.value, "course.id")
|
||||
uniqBy(courseSessions.value, "course.id")
|
||||
);
|
||||
|
||||
const courseSessionForRoute = computed(() => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { itGetCached } from "@/fetchHelpers";
|
|||
import { LearningPath } from "@/services/learningPath";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import _ from "lodash";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type LearningPathStoreState = {
|
||||
|
|
@ -62,7 +62,7 @@ export const useLearningPathStore = defineStore({
|
|||
);
|
||||
|
||||
const learningPath = LearningPath.fromJson(
|
||||
_.cloneDeep(learningPathData),
|
||||
cloneDeep(learningPathData),
|
||||
completionData
|
||||
);
|
||||
this.learningPaths.set(key, learningPath);
|
||||
|
|
|
|||
Loading…
Reference in New Issue