Singe circle selection

This commit is contained in:
Reto Aebersold 2023-09-20 15:32:21 +02:00
parent 4d46426d4f
commit 4ba12cbec8
11 changed files with 242 additions and 237 deletions

View File

@ -22,8 +22,8 @@ onMounted(async () => {
log.debug("CockpitParentPage mounted", props.courseSlug); log.debug("CockpitParentPage mounted", props.courseSlug);
try { try {
await cockpitStore.loadCourseSessionUsers(courseSession.value.id); const members = await cockpitStore.loadCourseSessionMembers(courseSession.value.id);
cockpitStore.courseSessionUsers?.forEach((csu) => { members.forEach((csu) => {
competenceStore.loadCompetenceProfilePage( competenceStore.loadCompetenceProfilePage(
props.courseSlug + "-competencenavi-competences", props.courseSlug + "-competencenavi-competences",
csu.user_id csu.user_id
@ -31,7 +31,11 @@ onMounted(async () => {
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id); learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
}); });
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id); await learningPathStore.loadLearningPath(
props.courseSlug + "-lp",
useUserStore().id
);
await cockpitStore.loadCircles(props.courseSlug, courseSession.value.id);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
} }

View File

@ -19,7 +19,7 @@ onMounted(async () => {
}); });
const user = computed(() => { const user = computed(() => {
return cockpitStore.courseSessionUsers?.find((csu) => csu.user_id === props.userId); return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId);
}); });
</script> </script>

View File

@ -28,7 +28,7 @@ const learningPath = computed(() => {
}); });
const user = computed(() => { const user = computed(() => {
return cockpitStore.courseSessionUsers?.find((csu) => csu.user_id === props.userId); return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId);
}); });
function setActiveClasses(isActive: boolean) { function setActiveClasses(isActive: boolean) {

View File

@ -85,10 +85,10 @@ const assignmentDetail = computed(() =>
/> />
</div> </div>
<div v-if="cockpitStore.courseSessionUsers?.length" class="mt-6"> <div v-if="cockpitStore.courseSessionMembers?.length" class="mt-6">
<ul> <ul>
<ItPersonRow <ItPersonRow
v-for="csu in cockpitStore.courseSessionUsers" v-for="csu in cockpitStore.courseSessionMembers"
:key="csu.user_id + csu.session_title" :key="csu.user_id + csu.session_title"
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"

View File

@ -167,7 +167,7 @@ watch(
<div class="mt-4 flex flex-col bg-white p-6"> <div class="mt-4 flex flex-col bg-white p-6">
<div <div
v-for="(csu, index) in cockpitStore.courseSessionUsers" v-for="(csu, index) in cockpitStore.courseSessionMembers"
:key="csu.user_id + csu.session_title" :key="csu.user_id + csu.session_title"
> >
<ItPersonRow <ItPersonRow

View File

@ -9,8 +9,9 @@ const courseSession = useCurrentCourseSession();
const circleDates = computed(() => { const circleDates = computed(() => {
const dueDates = courseSession.value.due_dates.filter((dueDate) => { const dueDates = courseSession.value.due_dates.filter((dueDate) => {
return cockpitStore.selectedCircles.includes( if (!cockpitStore.selectedCircle) return false;
dueDate?.circle?.translation_key ?? "" return (
cockpitStore.selectedCircle.translation_key == dueDate?.circle?.translation_key
); );
}); });
return dueDates.slice(0, 4); return dueDates.slice(0, 4);

View File

@ -8,11 +8,10 @@ import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue"; import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -20,54 +19,19 @@ const props = defineProps<{
log.debug("CockpitIndexPage created", props.courseSlug); log.debug("CockpitIndexPage created", props.courseSlug);
const userStore = useUserStore();
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore(); const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
function userCountStatusForCircle(userId: string, translationKey: string) { function userCountStatusForCircle(userId: string, translationKey: string) {
const criteria = competenceStore.flatPerformanceCriteria( if (!cockpitStore.selectedCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
userId, const criteria = competenceStore.flatPerformanceCriteria(userId, [
cockpitStore.selectedCircles cockpitStore.selectedCircle.translation_key,
); ]);
const grouped = groupBy(criteria, "circle.translation_key"); const grouped = groupBy(criteria, "circle.translation_key");
return competenceStore.calcStatusCount(grouped[translationKey] as []); return competenceStore.calcStatusCount(grouped[translationKey] as []);
} }
const circles = computed(() => {
const learningPathCircles = learningPathStore
.learningPathForUser(props.courseSlug, userStore.id)
?.circles.map((c) => {
return {
id: c.id,
title: c.title,
slug: c.slug,
translation_key: c.translation_key,
};
});
if (cockpitStore.cockpitSessionUser?.circles?.length) {
return cockpitStore.cockpitSessionUser.circles;
} else if (learningPathCircles) {
return learningPathCircles;
} else {
return [];
}
});
const selectedCirclesTitles = computed(() => {
return circles.value
.filter((c) => cockpitStore.selectedCircles.includes(c.translation_key))
.map((c) => c.title) as string[];
});
function setActiveClasses(translationKey: string) {
return cockpitStore.selectedCircles.indexOf(translationKey) > -1
? ["bg-blue-900", "text-white"]
: ["text-bg-900"];
}
</script> </script>
<template> <template>
@ -76,161 +40,160 @@ function setActiveClasses(translationKey: string) {
<div class="mb-9 flex items-end justify-between"> <div class="mb-9 flex items-end justify-between">
<h1>Cockpit</h1> <h1>Cockpit</h1>
<div class="flex flex-row"> <div class="flex flex-row">
<p class="text-base">{{ $t("general.circles") }}:</p> <ItDropdownSelect
<ul class="ml-4 flex flex-row text-base font-bold leading-7"> v-model="cockpitStore.selectedCircle"
<li class="mt-4 w-full lg:mt-0 lg:w-96"
v-for="circle in circles" :items="cockpitStore.circles"
:key="circle.translation_key" ></ItDropdownSelect>
class="mr-4 last:mr-0" </div>
> </div>
<button <template v-if="cockpitStore.selectedCircle">
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0" <!-- Status -->
:class="setActiveClasses(circle.translation_key)" <div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
@click="cockpitStore.toggleCircleSelection(circle.translation_key)" <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("Trainerunterlagen") }}
</h3>
<div class="mb-4">
{{ $t("cockpit.trainerFilesText") }}
</div>
</div>
<div>
<a
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
class="btn-secondary min-w-min"
target="_blank"
> >
{{ circle.title }} {{ $t("MS Teams öffnen") }}
</button> </a>
</li>
</ul>
</div>
</div>
<!-- Status -->
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("Trainerunterlagen") }}
</h3>
<div class="mb-4">
{{ $t("cockpit.trainerFilesText") }}
</div> </div>
</div> </div>
<div> <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<a <div>
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI" <h3 class="heading-3 mb-4 flex items-center gap-2">
class="btn-secondary min-w-min" {{ $t("Anwesenheitskontrolle Präsenzkurse") }}
target="_blank" </h3>
> <div class="mb-4">
{{ $t("MS Teams öffnen") }} {{
</a> $t(
</div> "Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
</div> )
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"> }}
<div> </div>
<h3 class="heading-3 mb-4 flex items-center gap-2"> </div>
{{ $t("Anwesenheitskontrolle Präsenzkurse") }} <div>
</h3> <router-link
<div class="mb-4"> :to="`/course/${props.courseSlug}/cockpit/attendance`"
{{ class="btn-secondary min-w-min"
$t( >
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden." {{ $t("Anwesenheit prüfen") }}
) </router-link>
}}
</div> </div>
</div> </div>
<div> <div class="bg-white p-6">
<router-link <CockpitDates></CockpitDates>
:to="`/course/${props.courseSlug}/cockpit/attendance`"
class="btn-secondary min-w-min"
>
{{ $t("Anwesenheit prüfen") }}
</router-link>
</div> </div>
</div> </div>
<div class="bg-white p-6"> <SubmissionsOverview
<CockpitDates></CockpitDates> :course-session="courseSession"
</div> :selected-circle="cockpitStore.selectedCircle.id"
</div> ></SubmissionsOverview>
<SubmissionsOverview <div class="pt-4">
:course-session="courseSession" <!-- progress -->
:selected-circles="selectedCirclesTitles" <div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6">
></SubmissionsOverview> <h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<div class="pt-4"> <ul>
<!-- progress --> <ItPersonRow
<div v-if="cockpitStore.courseSessionUsers" class="bg-white p-6"> v-for="csu in cockpitStore.courseSessionMembers"
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1> :key="csu.user_id + csu.session_title"
<ul> :name="`${csu.first_name} ${csu.last_name}`"
<ItPersonRow :avatar-url="csu.avatar_url"
v-for="csu in cockpitStore.courseSessionUsers" >
:key="csu.user_id + csu.session_title" <template #center>
:name="`${csu.first_name} ${csu.last_name}`" <div
:avatar-url="csu.avatar_url" class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
> >
<template #center> <LearningPathDiagram
<div v-if="
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row" learningPathStore.learningPathForUser(
> props.courseSlug,
<ul class="w-full"> csu.user_id
<li )
v-for="(circle, i) of cockpitStore.selectedCircles" "
:key="i" :learning-path="
class="flex flex-col justify-between lg:h-12 lg:flex-row lg:items-center"
>
<LearningPathDiagram
v-if="
learningPathStore.learningPathForUser(
props.courseSlug,
csu.user_id
)
"
:learning-path="
learningPathStore.learningPathForUser( learningPathStore.learningPathForUser(
props.courseSlug, props.courseSlug,
csu.user_id csu.user_id
) as LearningPath ) as LearningPath
" "
:postfix="`cockpit-${csu.user_id}-${i}`" :profile-user-id="`${csu.user_id}`"
:profile-user-id="`${csu.user_id}`" :show-circle-translation-keys="[
:show-circle-translation-keys="[circle]" cockpitStore.selectedCircle.translation_key,
:pull-up="false" ]"
diagram-type="singleSmall" :pull-up="false"
class="mr-4" diagram-type="singleSmall"
></LearningPathDiagram> class="mr-4"
<p class="lg:min-w-[150px]"> ></LearningPathDiagram>
{{ selectedCirclesTitles[i] }} <p class="lg:min-w-[150px]">
</p> {{ cockpitStore.selectedCircle.name }}
<div class="ml-4 flex flex-row items-center"> </p>
<div class="mr-6 flex flex-row items-center"> <div class="ml-4 flex flex-row items-center">
<it-icon-smiley-thinking <div class="mr-6 flex flex-row items-center">
class="mr-2 inline-block h-8 w-8" <it-icon-smiley-thinking
></it-icon-smiley-thinking> class="mr-2 inline-block h-8 w-8"
<p class="text-bold inline-block w-6"> ></it-icon-smiley-thinking>
{{ userCountStatusForCircle(csu.user_id, circle).FAIL }} <p class="text-bold inline-block w-6">
</p> {{
</div> userCountStatusForCircle(
<li class="mr-6 flex flex-row items-center"> csu.user_id,
<it-icon-smiley-happy cockpitStore.selectedCircle.translation_key
class="mr-2 inline-block h-8 w-8" ).FAIL
></it-icon-smiley-happy> }}
<p class="text-bold inline-block w-6"> </p>
{{ 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 w-6">
{{ userCountStatusForCircle(csu.user_id, circle).UNKNOWN }}
</p>
</li>
</div> </div>
</li> <li class="mr-6 flex flex-row items-center">
</ul> <it-icon-smiley-happy
</div> class="mr-2 inline-block h-8 w-8"
</template> ></it-icon-smiley-happy>
<template #link> <p class="text-bold inline-block w-6">
<router-link {{
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`" userCountStatusForCircle(
class="link w-full lg:text-right" csu.user_id,
> cockpitStore.selectedCircle.translation_key
{{ $t("general.profileLink") }} ).SUCCESS
</router-link> }}
</template> </p>
</ItPersonRow> </li>
</ul> <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 w-6">
{{
userCountStatusForCircle(
csu.user_id,
cockpitStore.selectedCircle.translation_key
).UNKNOWN
}}
</p>
</li>
</div>
</div>
</template>
<template #link>
<router-link
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
class="link w-full lg:text-right"
>
{{ $t("general.profileLink") }}
</router-link>
</template>
</ItPersonRow>
</ul>
</div>
</div> </div>
</div> </template>
</div> </div>
</div> </div>
</template> </template>

View File

@ -18,7 +18,7 @@ const cockpitStore = useCockpitStore();
const completeFeedbacks = ref(0); const completeFeedbacks = ref(0);
const numFeedbacks = computed(() => { const numFeedbacks = computed(() => {
return cockpitStore.courseSessionUsers?.length ?? 0; return cockpitStore.courseSessionMembers?.length ?? 0;
}); });
onMounted(async () => { onMounted(async () => {

View File

@ -27,7 +27,7 @@ interface Submittable {
const props = defineProps<{ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
selectedCircles: string[]; selectedCircle: number;
}>(); }>();
log.debug("SubmissionsOverview created", props.courseSession.id); log.debug("SubmissionsOverview created", props.courseSession.id);
@ -46,7 +46,7 @@ const submittables = computed(() => {
return []; return [];
} }
return learningPath.circles return learningPath.circles
.filter((circle) => props.selectedCircles.includes(circle.title)) .filter((circle) => props.selectedCircle == circle.id)
.flatMap((circle) => { .flatMap((circle) => {
const learningContents = circle.flatLearningContents.filter( const learningContents = circle.flatLearningContents.filter(
(lc) => (lc) =>
@ -128,7 +128,7 @@ const getIconName = (lc: LearningContent) => {
<template> <template>
<div class="bg-white px-6 py-2"> <div class="bg-white px-6 py-2">
<div v-if="cockpitStore.courseSessionUsers" class="divide-y divide-gray-500"> <div v-if="cockpitStore.courseSessionMembers" class="divide-y divide-gray-500">
<div <div
v-for="submittable in submittables" v-for="submittable in submittables"
:key="submittable.id" :key="submittable.id"

View File

@ -42,7 +42,9 @@ export async function loadAssignmentCompletionStatusData(
`/api/assignment/${assignmentId}/${courseSessionId}/status/` `/api/assignment/${assignmentId}/${courseSessionId}/status/`
)) as UserAssignmentCompletionStatus[]; )) as UserAssignmentCompletionStatus[];
const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId); const courseSessionUsers = await cockpitStore.loadCourseSessionMembers(
courseSessionId
);
const gradedUsers: GradedUser[] = []; const gradedUsers: GradedUser[] = [];
const assignmentSubmittedUsers: CourseSessionUser[] = []; const assignmentSubmittedUsers: CourseSessionUser[] = [];

View File

@ -2,27 +2,58 @@ import { itGetCached } from "@/fetchHelpers";
import type { CourseSessionUser, ExpertSessionUser } from "@/types"; import type { CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel"; import log from "loglevel";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CockpitStoreState = { export type CockpitStoreState = {
courseSessionUsers: CourseSessionUser[] | undefined; courseSessionMembers: CourseSessionUser[] | undefined;
cockpitSessionUser: ExpertSessionUser | undefined; selectedCircle:
selectedCircles: string[]; | {
id: number;
name: string;
slug: string;
translation_key: string;
}
| undefined;
circles: {
id: number;
name: string;
slug: string;
translation_key: string;
}[];
}; };
export const useCockpitStore = defineStore({ export const useCockpitStore = defineStore({
id: "cockpit", id: "cockpit",
state: () => { state: () => {
return { return {
courseSessionUsers: undefined, courseSessionMembers: undefined,
cockpitSessionUser: undefined, selectedCircle: undefined,
selectedCircles: [], circles: [],
} as CockpitStoreState; } as CockpitStoreState;
}, },
actions: { actions: {
async loadCourseSessionUsers(courseSessionId: number, reload = false) { async loadCircles(courseSlug: string, courseSessionId: number) {
log.debug("loadCockpitData called"); log.debug("loadCircles called");
const f = await courseCircles(courseSlug, courseSessionId);
this.circles = f.map((c) => {
return {
id: c.id,
name: c.title,
slug: c.slug,
translation_key: c.translation_key,
};
});
if (this.circles.length > 0) {
this.selectedCircle = this.circles[0];
}
},
async loadCourseSessionMembers(courseSessionId: number, reload = false) {
log.debug("loadCourseSessionMembers called");
const users = (await itGetCached( const users = (await itGetCached(
`/api/course/sessions/${courseSessionId}/users/`, `/api/course/sessions/${courseSessionId}/users/`,
{ {
@ -30,38 +61,42 @@ export const useCockpitStore = defineStore({
} }
)) as CourseSessionUser[]; )) as CourseSessionUser[];
this.courseSessionUsers = users.filter((user) => user.role === "MEMBER"); this.courseSessionMembers = users.filter((user) => user.role === "MEMBER");
return this.courseSessionMembers;
const userStore = useUserStore();
const currentUser = users.find((user) => user.user_id === userStore.id);
if (currentUser && currentUser.role === "EXPERT") {
this.cockpitSessionUser = currentUser as ExpertSessionUser;
}
if (this.selectedCircles.length === 0) {
// workaround to select first circle by default, when nothing is selected...
// TODO: is this the right place to do this?
if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
}
}
if (!this.courseSessionUsers) {
throw `No courseSessionUsers data found for user`;
}
return this.courseSessionUsers;
},
toggleCircleSelection(translationKey: string) {
if (this.selectedCircles.indexOf(translationKey) < 0) {
this.selectedCircles.push(translationKey);
} else {
if (this.selectedCircles.length === 1) {
return;
}
const index = this.selectedCircles.indexOf(translationKey);
this.selectedCircles.splice(index, 1);
}
}, },
}, },
}); });
async function courseCircles(courseSlug: string, courseSessionId: number) {
const userStore = useUserStore();
const userId = userStore.id;
const users = (await itGetCached(`/api/course/sessions/${courseSessionId}/users/`, {
reload: false,
})) as CourseSessionUser[];
// First check if current user is an expert for this course session
const currentUser = users.find((user) => user.user_id === userId);
if (currentUser && currentUser.role === "EXPERT") {
const expert = currentUser as ExpertSessionUser;
return expert.circles;
}
// Return all circles from learning path for admin users
if (userStore.is_superuser) {
const learningPathStore = useLearningPathStore();
const learningPathCircles = learningPathStore
.learningPathForUser(courseSlug, userId)
?.circles.map((c) => {
return {
id: c.id,
title: c.title,
slug: c.slug,
translation_key: c.translation_key,
};
});
return learningPathCircles || [];
}
return [];
}