Merged in feature/dashboard-daniel (pull request #317)

Feature/dashboard daniel
This commit is contained in:
Christian Cueni 2024-05-01 05:41:03 +00:00
commit a738889b01
124 changed files with 5298 additions and 1344 deletions

View File

@ -1,31 +0,0 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import { computed } from "vue";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
const expertCockpitStore = useExpertCockpitStore();
const courseSession = useCurrentCourseSession();
const circleDates = computed(() => {
const dueDates = courseSession.value.due_dates.filter((dueDate) => {
if (!expertCockpitStore.currentCircle) return false;
return expertCockpitStore.currentCircle.id == dueDate?.circle?.id;
});
return dueDates.slice(0, 4);
});
</script>
<template>
<div class="flex flex-col space-y-2">
<h3 class="heading-3">{{ $t("Nächste Termine") }}</h3>
<div
v-for="dueDate in circleDates"
:key="dueDate.id"
class="border-t border-gray-500 pt-2"
>
<DueDateSingle :due-date="dueDate" :single-line="true"></DueDateSingle>
</div>
<div v-if="circleDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
</div>
</template>

View File

@ -1,4 +1,3 @@
2
<script setup lang="ts"> <script setup lang="ts">
import AssignmentSubmissionProgress from "@/components/assignment/AssignmentSubmissionProgress.vue"; import AssignmentSubmissionProgress from "@/components/assignment/AssignmentSubmissionProgress.vue";
import type { import type {

View File

@ -24,14 +24,14 @@ const progress = computed(() => ({
<div class="flex items-center"> <div class="flex items-center">
<i18next :translation="$t('a.NUMBER Elemente abgeschlossen')"> <i18next :translation="$t('a.NUMBER Elemente abgeschlossen')">
<template #NUMBER> <template #NUMBER>
<span class="mr-3 text-4xl font-bold">{{ totalAssignments }}</span> <span class="mr-3 text-xl font-bold">{{ totalAssignments }}</span>
</template> </template>
</i18next> </i18next>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<i18next :translation="$t('a.xOfY Punkten erreicht')"> <i18next :translation="$t('a.xOfY Punkten erreicht')">
<template #xOfY> <template #xOfY>
<span class="mr-3 text-4xl font-bold"> <span class="mr-3 text-xl font-bold">
{{ {{
$t("a.VALUE von MAXIMUM", { $t("a.VALUE von MAXIMUM", {
VALUE: props.achievedPointsCount, VALUE: props.achievedPointsCount,

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { computed, onMounted, ref } from "vue";
import type { ProgressDashboardAssignmentType } from "@/gql/graphql";
import { fetchProgressData } from "@/services/dashboard";
import AssignmentProgressSummaryBox from "@/components/dashboard/AssignmentProgressSummaryBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
sessionToContinueId: string;
}>();
const DEFAULT_ASSIGNMENT = {
_id: "",
points_achieved_count: 0,
points_max_count: 0,
total_count: 0,
};
const assignment: Ref<ProgressDashboardAssignmentType | null> = ref(DEFAULT_ASSIGNMENT);
const competenceCertificateUrl = computed(() => {
return `/course/${props.courseSlug}/competence/certificates?courseSessionId=${props.sessionToContinueId}`;
});
onMounted(async () => {
const data = await fetchProgressData(props.courseId);
assignment.value = data?.assignment ?? null;
});
</script>
<template>
<div v-if="assignment">
<div class="w-[395px]">
<AssignmentProgressSummaryBox
:total-assignments="assignment.total_count"
:achieved-points-count="assignment.points_achieved_count"
:max-points-count="assignment.points_max_count"
:details-link="competenceCertificateUrl"
/>
</div>
</div>
</template>

View File

@ -6,6 +6,7 @@ import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{ const props = defineProps<{
assignmentsCompleted: number; assignmentsCompleted: number;
avgPassed: number; avgPassed: number;
courseSlug: string;
}>(); }>();
const progress = computed(() => { const progress = computed(() => {
@ -19,7 +20,7 @@ const progress = computed(() => {
<template> <template>
<BaseBox <BaseBox
:details-link="'/statistic/assignment'" :details-link="`/statistic/${courseSlug}/assignment`"
data-cy="dashboard.stats.assignments" data-cy="dashboard.stats.assignments"
> >
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template> <template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>

View File

@ -6,6 +6,7 @@ import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{ const props = defineProps<{
daysCompleted: number; daysCompleted: number;
avgParticipantsPresent: number; avgParticipantsPresent: number;
courseSlug: string;
}>(); }>();
const progressRecord = computed(() => { const progressRecord = computed(() => {
@ -18,7 +19,10 @@ const progressRecord = computed(() => {
</script> </script>
<template> <template>
<BaseBox :details-link="'/statistic/attendance'" data-cy="dashboard.stats.attendance"> <BaseBox
:details-link="`/statistic/${props.courseSlug}/attendance`"
data-cy="dashboard.stats.attendance"
>
<template #title>{{ $t("a.Anwesenheit") }}</template> <template #title>{{ $t("a.Anwesenheit") }}</template>
<template #content> <template #content>
<div class="flex items-center"> <div class="flex items-center">

View File

@ -5,7 +5,7 @@ defineProps<{
</script> </script>
<template> <template>
<div class="flex flex-col space-y-4 bg-white p-6"> <div class="flex h-full flex-col space-y-4 bg-white">
<h4 class="mb-1 font-bold"> <h4 class="mb-1 font-bold">
<slot name="title"></slot> <slot name="title"></slot>
</h4> </h4>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { computed, onMounted, ref } from "vue";
import type { ProgressDashboardCompetenceType } from "@/gql/graphql";
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
import { fetchProgressData } from "@/services/dashboard";
const props = defineProps<{
courseId: string;
courseSlug: string;
sessionToContinueId: string;
}>();
const DEFAULT_COMPETENCE = { _id: "", total_count: 0, success_count: 0, fail_count: 0 };
const competence: Ref<ProgressDashboardCompetenceType | null> = ref(DEFAULT_COMPETENCE);
const competenceCriteriaUrl = computed(() => {
return `/course/${props.courseSlug}/competence/self-evaluation-and-feedback?courseSessionId=${props.sessionToContinueId}`;
});
onMounted(async () => {
const data = await fetchProgressData(props.courseId);
competence.value = data?.competence ?? null;
});
</script>
<template>
<div v-if="competence" class="w-[395px]">
<CompetenceSummaryBox
:fail-count="competence.fail_count"
:success-count="competence.success_count"
:details-link="competenceCriteriaUrl"
/>
</div>
</template>

View File

@ -13,11 +13,11 @@ defineProps<{
<template #title>{{ $t("a.Selbsteinschätzungen") }}</template> <template #title>{{ $t("a.Selbsteinschätzungen") }}</template>
<template #content> <template #content>
<div class="flex items-center"> <div class="flex items-center">
<it-icon-smiley-happy class="mr-4 h-12 w-12"></it-icon-smiley-happy> <it-icon-smiley-happy class="mr-4 h-[28px] w-[28px]"></it-icon-smiley-happy>
<i18next :translation="$t('a.{NUMBER} Das kann ich')"> <i18next :translation="$t('a.{NUMBER} Das kann ich')">
<template #NUMBER> <template #NUMBER>
<span <span
class="mr-3 text-4xl font-bold" class="mr-3 text-2xl font-bold"
data-cy="dashboard.stats.competence.success" data-cy="dashboard.stats.competence.success"
> >
{{ successCount }} {{ successCount }}
@ -26,11 +26,13 @@ defineProps<{
</i18next> </i18next>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<it-icon-smiley-thinking class="mr-4 h-12 w-12"></it-icon-smiley-thinking> <it-icon-smiley-thinking
class="mr-4 h-[28px] w-[28px]"
></it-icon-smiley-thinking>
<i18next :translation="$t('a.{NUMBER} Das will ich nochmals anschauen')"> <i18next :translation="$t('a.{NUMBER} Das will ich nochmals anschauen')">
<template #NUMBER> <template #NUMBER>
<span <span
class="mr-3 text-4xl font-bold" class="mr-3 text-2xl font-bold"
data-cy="dashboard.stats.competence.fail" data-cy="dashboard.stats.competence.fail"
> >
{{ failCount }} {{ failCount }}

View File

@ -1,45 +0,0 @@
<script setup lang="ts">
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useDashboardStore } from "@/stores/dashboard";
import { getCockpitUrl } from "@/utils/utils";
const dashboardStore = useDashboardStore();
const courseSessionsStore = useCourseSessionsStore();
const allDueDates = courseSessionsStore.allDueDates();
</script>
<template>
<template v-if="dashboardStore.currentDashboardConfig">
<h4 class="mb-6 text-xl font-bold">{{ $t("a.Aktueller Lehrgang") }}</h4>
<div class="mb-6 border border-gray-300 p-6">
<h3 class="mb-6">{{ dashboardStore.currentDashboardConfig.name }}</h3>
<router-link
class="btn-blue"
target="_blank"
:to="getCockpitUrl(dashboardStore.currentDashboardConfig.slug)"
>
{{ $t("a.Cockpit anschauen") }}
</router-link>
</div>
</template>
<router-link
v-if="dashboardStore.dashboardConfigs.length > 1"
class="block text-sm underline"
to="/statistic/list"
>
{{ $t("a.Alle Lehrgänge anzeigen") }}
</router-link>
<h3 class="mb-6 mt-16 text-xl font-bold">{{ $t("a.AlleTermine") }}</h3>
<DueDatesList
:due-dates="allDueDates"
:max-count="13"
:show-top-border="true"
:show-all-due-dates-link="true"
:show-bottom-border="true"
:show-course-session="true"
></DueDatesList>
</template>

View File

@ -0,0 +1,174 @@
<script setup lang="ts">
import { computed } from "vue";
import type { DashboardCourseConfigType, WidgetType } from "@/services/dashboard";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue";
import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue";
import MentorOpenTasksCount from "@/components/dashboard/MentorOpenTasksCount.vue";
import MentorMenteeCount from "@/components/dashboard/MentorMenteeCount.vue";
import MentorCompetenceSummary from "@/components/dashboard/MentorCompetenceSummary.vue";
import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils";
import UkStatistics from "@/components/dashboard/UkStatistics.vue";
const mentorWidgets = [
"MentorTasksWidget",
"MentorPersonWidget",
"MentorCompetenceWidget",
];
const progressWidgets = ["CompetenceWidget", "CompetenceCertificateWidget"];
const props = defineProps<{
courseConfig: DashboardCourseConfigType | undefined;
}>();
const courseSlug = computed(() => props.courseConfig?.course_slug ?? "");
const courseName = computed(() => props.courseConfig?.course_title ?? "");
const numberOfMentorWidgets = computed(() => {
return (
props.courseConfig?.widgets?.filter((widget) => mentorWidgets.includes(widget))
.length ?? 0
);
});
const numberOfProgressWidgets = computed(() => {
return (
props.courseConfig?.widgets?.filter((widget) => progressWidgets.includes(widget))
.length ?? 0
);
});
function hasWidget(widget: WidgetType) {
return props.courseConfig?.widgets?.includes(widget) ?? false;
}
const actionButtonProps = computed<{ href: string; text: string; cyKey: string }>(
() => {
if (props.courseConfig?.role_key === "Member") {
return {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "Weiter lernen",
cyKey: "progress-dashboard-continue-course-link",
};
} else if (props.courseConfig?.role_key === "Expert") {
return {
href: getCockpitUrl(props.courseConfig?.course_slug),
text: "Cockpit anschauen",
cyKey: "cockpit-dashboard-link",
};
} else if (props.courseConfig?.role_key === "Supervisor") {
return {
href: getCockpitUrl(props.courseConfig?.course_slug),
text: "Cockpit anschauen",
cyKey: "cockpit-dashboard-link",
};
} else if (props.courseConfig?.role_key === "MentorVV") {
return {
href: getLearningMentorUrl(props.courseConfig?.course_slug),
text: "a.Übersicht anschauen",
cyKey: "lm-dashboard-link",
};
}
return {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "Weiter lernen",
cyKey: "progress-dashboard-continue-course-link",
};
}
);
function hasActionButton(): boolean {
return props.courseConfig?.role_key !== "MentorUK";
}
</script>
<template>
<div v-if="courseConfig" class="mb-14 space-y-8">
<div class="flex flex-col space-y-8 bg-white p-6">
<div class="border-b border-gray-300 pb-8">
<div class="flex flex-row items-start justify-between">
<h3 class="mb-4 text-3xl" data-cy="db-course-title">{{ courseName }}</h3>
<a
v-if="hasActionButton()"
:href="actionButtonProps.href"
class="btn-blue"
:data-cy="actionButtonProps.cyKey"
>
{{ $t(actionButtonProps.text) }}
</a>
</div>
<p>
<span class="rounded bg-gray-300 px-2 py-1">
{{ $t(courseConfig.role_key) }}
</span>
<router-link
v-if="courseConfig.has_preview"
:to="getLearningPathUrl(courseConfig.course_slug)"
class="inline-block pl-6"
target="_blank"
>
<div class="flex items-center">
<span>{{ $t("a.VorschauTeilnehmer") }}</span>
<it-icon-external-link class="ml-1 !h-4 !w-4" />
</div>
</router-link>
</p>
</div>
<div
v-if="
hasWidget('ProgressWidget') &&
courseConfig.session_to_continue_id &&
courseSlug
"
class="border-b border-gray-300 pb-8 last:border-0"
>
<LearningPathDiagram
:key="courseSlug"
:course-slug="courseSlug"
:course-session-id="courseConfig.session_to_continue_id"
diagram-type="horizontal"
></LearningPathDiagram>
</div>
<div
v-if="numberOfProgressWidgets"
class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<AssignmentSummary
v-if="hasWidget('CompetenceCertificateWidget')"
:course-slug="courseSlug"
:session-to-continue-id="courseConfig.session_to_continue_id"
:course-id="courseConfig.course_id"
/>
<CompetenceSummary
v-if="hasWidget('CompetenceWidget')"
:course-slug="courseSlug"
:session-to-continue-id="courseConfig.session_to_continue_id"
:course-id="courseConfig.course_id"
></CompetenceSummary>
</div>
<div
v-if="hasWidget('UKStatisticsWidget')"
class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<UkStatistics :course-slug="courseSlug" :course-id="courseConfig.course_id" />
</div>
<div
v-if="numberOfMentorWidgets > 0"
class="flex flex-col flex-wrap items-stretch md:flex-row"
>
<MentorMenteeCount
v-if="hasWidget('MentorPersonWidget')"
:course-id="courseConfig.course_id"
:course-slug="courseConfig?.course_slug"
/>
<MentorOpenTasksCount
v-if="hasWidget('MentorTasksWidget')"
:course-id="courseConfig.course_id"
:course-slug="courseSlug"
/>
<MentorCompetenceSummary
v-if="hasWidget('MentorCompetenceWidget')"
:course-id="courseConfig.course_id"
/>
</div>
</div>
</div>
</template>

View File

@ -7,6 +7,7 @@ const props = defineProps<{
feedbackCount: number; feedbackCount: number;
statisfactionMax: number; statisfactionMax: number;
statisfactionAvg: number; statisfactionAvg: number;
courseSlug: string;
}>(); }>();
const satisfactionColor = computed(() => { const satisfactionColor = computed(() => {
@ -15,7 +16,10 @@ const satisfactionColor = computed(() => {
</script> </script>
<template> <template>
<BaseBox :details-link="'/statistic/feedback'" data-cy="dashboard.stats.feedback"> <BaseBox
:details-link="`/statistic/${courseSlug}/feedback`"
data-cy="dashboard.stats.feedback"
>
<template #title>{{ $t("a.Feedback Teilnehmer") }}</template> <template #title>{{ $t("a.Feedback Teilnehmer") }}</template>
<template #content> <template #content>
<div class="flex items-center"> <div class="flex items-center">

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { onMounted, ref } from "vue";
import { fetchMentorCompetenceSummary } from "@/services/dashboard";
import type { AssignmentsStatisticsType } from "@/gql/graphql";
import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{
courseId: string;
}>();
const summary: Ref<AssignmentsStatisticsType | null> = ref(null);
onMounted(async () => {
summary.value = await fetchMentorCompetenceSummary(props.courseId);
console.log(summary.value);
});
</script>
<template>
<div v-if="summary" class="w-[325px]">
<BaseBox
:details-link="`/dashboard/persons-competence?course=${props.courseId}`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("Kompetenznachweise") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white">
<div
class="flex h-[47px] items-center justify-center py-1 pr-3 text-3xl font-bold"
>
<span>{{ summary.summary.total_passed }}</span>
</div>
<p class="ml-3 mt-0 leading-[47px]">{{ $t("Bestanden") }}</p>
</div>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[47px] items-center justify-center py-1 pr-3 text-3xl font-bold"
>
<span>{{ summary.summary.total_failed }}</span>
</div>
<p class="ml-3 mt-0 leading-[47px]">{{ $t("Nicht bestanden") }}</p>
</div>
</template>
</BaseBox>
</div>
</template>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { onMounted, ref } from "vue";
import { fetchMenteeCount } from "@/services/dashboard";
import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
}>();
const menteeCount: Ref<number> = ref(0);
onMounted(async () => {
const data = await fetchMenteeCount(props.courseId);
menteeCount.value = data?.mentee_count;
});
</script>
<template>
<div class="w-[325px]">
<BaseBox
:details-link="`/dashboard/persons?course=${props.courseId}`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("a.Personen") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
>
<span>{{ menteeCount }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">
{{ $t("a.Personen, die du begleitest") }}
</p>
</div>
</template>
</BaseBox>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { onMounted, ref } from "vue";
import { fetchOpenTasksCount } from "@/services/dashboard";
import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
}>();
const openTaskCount: Ref<number> = ref(0);
onMounted(async () => {
const data = await fetchOpenTasksCount(props.courseId);
openTaskCount.value = data?.open_task_count;
});
</script>
<template>
<div class="w-[325px]">
<BaseBox
:details-link="`/course/${props.courseSlug}/learning-mentor/tasks`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("Zu erledigen") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] w-[74px] items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-3xl font-bold"
>
<span>{{ openTaskCount }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">{{ $t("Elemente zu erledigen") }}</p>
</div>
</template>
</BaseBox>
</div>
</template>

View File

@ -1,19 +0,0 @@
<script setup lang="ts">
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
const courseSessionsStore = useCourseSessionsStore();
const allDueDates = courseSessionsStore.allDueDates();
</script>
<template>
<h4 class="mb-6 text-xl font-bold">{{ $t("a.AlleTermine") }}</h4>
<DueDatesList
:due-dates="allDueDates"
:max-count="13"
:show-top-border="true"
:show-all-due-dates-link="true"
:show-bottom-border="true"
:show-course-session="true"
></DueDatesList>
</template>

View File

@ -1,65 +1,97 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard"; import { useDashboardStore } from "@/stores/dashboard";
import { computed } from "vue"; import { computed, onMounted, ref } from "vue";
import CourseStatistics from "@/components/dashboard/CourseStatistics.vue";
import AttendanceSummaryBox from "@/components/dashboard/AttendanceSummaryBox.vue"; import AttendanceSummaryBox from "@/components/dashboard/AttendanceSummaryBox.vue";
import type { CourseStatisticsType } from "@/gql/graphql"; import type { CourseStatisticsType } from "@/gql/graphql";
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue"; import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue"; import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue";
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue"; import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
}>();
const statistics = ref<CourseStatisticsType | null>(null);
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const courseSessionSelectionMetrics = computed(() => {
return statistics.value.course_session_selection_metrics;
});
const attendanceDayPresences = computed(() => { const attendanceDayPresences = computed(() => {
return statistics.value.attendance_day_presences.summary; return (
statistics?.value?.attendance_day_presences?.summary ?? {
days_completed: 0,
participants_present: 0,
}
);
}); });
const assigmentSummary = computed(() => { const assigmentSummary = computed(() => {
return statistics.value.assignments.summary; return (
statistics?.value?.assignments.summary ?? {
average_passed: 0,
completed_count: 0,
total_passed: 0,
total_failed: 0,
}
);
}); });
const competenceSummary = computed(() => { const competenceSummary = computed(() => {
return statistics.value.competences.summary; return (
statistics?.value?.competences.summary ?? {
fail_total: 0,
success_total: 0,
}
);
}); });
const feebackSummary = computed(() => { const feebackSummary = computed(() => {
return statistics.value.feedback_responses.summary; return (
statistics?.value?.feedback_responses.summary ?? {
satisfaction_average: 0,
satisfaction_max: 0,
total_responses: 0,
}
);
});
onMounted(async () => {
statistics.value = await dashboardStore.loadStatisticsDatav2(props.courseId);
}); });
</script> </script>
<template> <template>
<div v-if="statistics" class="mb-14 space-y-8"> <div v-if="statistics" class="space-y-8">
<CourseStatistics <div
:session-count="courseSessionSelectionMetrics.session_count" class="flex flex-col flex-wrap justify-between gap-x-5 border-b border-gray-300 pb-8 last:border-0 md:flex-row"
:participant-count="courseSessionSelectionMetrics.participant_count" >
:expert-count="courseSessionSelectionMetrics.expert_count"
/>
<div class="grid auto-rows-fr grid-cols-1 gap-8 xl:grid-cols-2">
<AttendanceSummaryBox <AttendanceSummaryBox
class="flex-grow"
:days-completed="attendanceDayPresences.days_completed" :days-completed="attendanceDayPresences.days_completed"
:avg-participants-present="attendanceDayPresences.participants_present" :avg-participants-present="attendanceDayPresences.participants_present"
:course-slug="props.courseSlug"
/> />
<AssignmentSummaryBox <AssignmentSummaryBox
class="flex-grow"
:assignments-completed="assigmentSummary.completed_count" :assignments-completed="assigmentSummary.completed_count"
:avg-passed="assigmentSummary.average_passed" :avg-passed="assigmentSummary.average_passed"
:course-slug="props.courseSlug"
/> />
</div>
<div
class="flex flex-col flex-wrap gap-x-5 border-b border-gray-300 align-top last:border-0 md:flex-row"
>
<FeedbackSummaryBox <FeedbackSummaryBox
:feedback-count="feebackSummary.total_responses" :feedback-count="feebackSummary.total_responses"
:statisfaction-max="feebackSummary.satisfaction_max" :statisfaction-max="feebackSummary.satisfaction_max"
:statisfaction-avg="feebackSummary.satisfaction_average" :statisfaction-avg="feebackSummary.satisfaction_average"
:course-slug="props.courseSlug"
/> />
<CompetenceSummaryBox <CompetenceSummaryBox
:fail-count="competenceSummary.fail_total" :fail-count="competenceSummary.fail_total"
:success-count="competenceSummary.success_total" :success-count="competenceSummary.success_total"
details-link="/statistic/competence" :details-link="`/statistic/${courseSlug}/competence`"
:course-slug="props.courseSlug"
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { useDashboardPersonsDueDates } from "@/composables";
import { computed } from "vue";
import _ from "lodash";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
const props = withDefaults(
defineProps<{
courseSessionId: string;
circleId?: string;
maxCount?: number;
showAllButton?: boolean;
}>(),
{
maxCount: 3,
circleId: undefined,
showAllButton: false,
}
);
const { loading, currentDueDates } = useDashboardPersonsDueDates();
const filteredDueDates = computed(() => {
let dueDates = currentDueDates.value.filter(
(dueDate) => dueDate.course_session_id === props.courseSessionId
);
if (props.circleId) {
dueDates = dueDates.filter((dueDate) => dueDate.circle?.id === props.circleId);
}
return _.take(dueDates, props.maxCount);
});
</script>
<template>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="flex flex-col space-y-2">
<h3 class="heading-3">{{ $t("Nächste Termine") }}</h3>
<div
v-for="dueDate in filteredDueDates"
:key="dueDate.id"
class="border-t border-gray-500 pt-2"
>
<DueDateSingle :due-date="dueDate"></DueDateSingle>
</div>
<div v-if="filteredDueDates.length === 0">
{{ $t("dueDates.noDueDatesAvailable") }}
</div>
</div>
<router-link
v-if="showAllButton"
class="btn-secondary mt-4"
:to="`/dashboard/due-dates?session=${courseSessionId}`"
>
{{ $t("a.Alle Termine anzeigen") }}
</router-link>
</template>

View File

@ -1,41 +1,46 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CourseSession, DueDate } from "@/types";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import dayjs from "dayjs"; import type { DashboardDueDate } from "@/services/dashboard";
import { computed } from "vue"; import { computed } from "vue";
import dayjs from "dayjs";
const props = defineProps<{ const props = defineProps<{
dueDate: DueDate; dueDate: DashboardDueDate;
singleLine?: boolean; singleLine?: boolean;
showCourseSession?: boolean;
}>(); }>();
const { t } = useTranslation(); const { t } = useTranslation();
const dateType = t(props.dueDate.date_type_translation_key); const dateType = t(props.dueDate.date_type_translation_key);
const assignmentType = t(props.dueDate.assignment_type_translation_key); const assignmentType = t(props.dueDate.assignment_type_translation_key);
const courseSessionsStore = useCourseSessionsStore(); const urlText = computed(() => {
const courseSession = courseSessionsStore.allCourseSessions.find( let result = "";
(cs: CourseSession) => cs.id === props.dueDate.course_session_id if (dateType) {
); result += dateType;
if (!courseSession) {
throw new Error("Course session not found");
}
const url = courseSession.actions.includes("expert-cockpit")
? props.dueDate.url_expert
: props.dueDate.url;
const courseSessionTitle = computed(() => {
if (props.dueDate.course_session_id) {
return (
courseSessionsStore.getCourseSessionById(props.dueDate.course_session_id)
?.title ?? ""
);
} }
return "";
if (assignmentType && !props.dueDate.title.startsWith(assignmentType)) {
result += " " + assignmentType;
}
if (props.dueDate.title) {
result += " " + props.dueDate.title;
}
return result.trim();
});
const showAsUrl = computed(() => {
return ["SUPERVISOR", "EXPERT", "MEMBER"].includes(
props.dueDate.course_session.my_role
);
});
const url = computed(() => {
if (["SUPERVISOR", "EXPERT"].includes(props.dueDate.course_session.my_role)) {
return props.dueDate.url_expert;
}
return props.dueDate.url;
}); });
</script> </script>
@ -46,34 +51,42 @@ const courseSessionTitle = computed(() => {
> >
<div class="space-y-1"> <div class="space-y-1">
<div> <div>
<a class="underline" :href="url"> <a v-if="showAsUrl" :href="url">
<span class="text-bold"> <span class="text-bold text-gray-900">
{{ dayjs(props.dueDate.start).format("D. MMMM YYYY") }}: {{ dayjs(props.dueDate.start).format("dddd D. MMMM YYYY") }}
<template v-if="dateType">
{{ dateType }}
</template>
<template v-else>
{{ assignmentType }}
</template>
{{ " " }}
</span> </span>
<template v-if="assignmentType && dateType">
{{ assignmentType }}:
{{ props.dueDate.title }}
</template>
<template v-else>
{{ props.dueDate.title }}
</template>
</a> </a>
<span v-else class="text-bold text-gray-900">
{{ dayjs(props.dueDate.start).format("dddd D. MMMM YYYY") }}
</span>
</div>
<div>
<a v-if="showAsUrl" class="underline" :href="url">
<span class="text-bold">
{{ urlText }}
</span>
</a>
<span v-else class="text-bold">
{{ urlText }}
</span>
</div> </div>
<div class="text-small text-gray-900"> <div class="text-small text-gray-900">
<div> <div>
<span v-if="props.showCourseSession ?? courseSessionTitle"> <span v-if="props.dueDate.course_session.is_uk">
{{ courseSessionTitle }}: {{ props.dueDate.course_session.session_title }}:
</span> </span>
{{ $t("a.Circle") }} «{{ props.dueDate.circle?.title }}» {{ $t("a.Circle") }} «{{ props.dueDate.circle?.title }}»
</div> </div>
</div> </div>
<div v-if="props.dueDate.persons?.length" class="flex gap-2">
<div v-for="person in props.dueDate.persons" :key="person.user_id">
<img
class="inline-block h-11 w-11 rounded-full"
:src="person.avatar_url_small || '/static/avatars/myvbv-default-avatar.png'"
:alt="`${person.first_name} ${person.last_name}`"
/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,54 +0,0 @@
<script lang="ts" setup>
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import type { DueDate } from "@/types";
import { computed } from "vue";
const props = defineProps<{
maxCount: number;
dueDates: DueDate[];
showTopBorder: boolean;
showBottomBorder: boolean;
showAllDueDatesLink: boolean;
showCourseSession: boolean;
}>();
const allDueDates = computed(() => {
return props.dueDates;
});
const dueDatesDisplayed = computed(() => {
return props.dueDates.slice(0, props.maxCount);
});
</script>
<template>
<div>
<ul :class="showBottomBorder ? '' : 'no-border-last'">
<li
v-for="dueDate in dueDatesDisplayed"
:key="dueDate.id"
class="cy-single-due-date"
:class="{ 'first:border-t': props.showTopBorder, 'border-b': true }"
>
<DueDateSingle
:due-date="dueDate"
:show-course-session="props.showCourseSession"
></DueDateSingle>
</li>
</ul>
<div v-if="allDueDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
<div
v-if="showAllDueDatesLink && allDueDates.length > 0"
class="flex items-center pt-6"
>
<a href="/appointments">{{ $t("dueDates.showAllDueDates") }}</a>
<it-icon-arrow-right />
</div>
</div>
</template>
<style lang="postcss" scoped>
.no-border-last li:last-child {
border-bottom: none !important;
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<div>
<DueDatesList
:due-dates="allDueDates"
:max-count="props.maxCount"
:show-top-border="props.showTopBorder"
show-all-due-dates-link
show-bottom-border
:show-course-session="false"
></DueDatesList>
</div>
</template>
<script lang="ts" setup>
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCurrentCourseSession } from "@/composables";
const props = defineProps<{
maxCount: number;
showTopBorder: boolean;
}>();
const courseSession = useCurrentCourseSession();
const allDueDates = courseSession.value.due_dates;
</script>

View File

@ -22,6 +22,7 @@ import {
getLearningPathUrl, getLearningPathUrl,
getMediaCenterUrl, getMediaCenterUrl,
} from "@/utils/utils"; } from "@/utils/utils";
import { useMentorTexts } from "@/composables";
log.debug("MainNavigationBar created"); log.debug("MainNavigationBar created");
@ -59,9 +60,9 @@ const selectedCourseSessionTitle = computed(() => {
const appointmentsUrl = computed(() => { const appointmentsUrl = computed(() => {
const currentCourseSession = courseSessionsStore.currentCourseSession; const currentCourseSession = courseSessionsStore.currentCourseSession;
if (currentCourseSession) { if (currentCourseSession) {
return `/course/${currentCourseSession.course.slug}/appointments`; return `/dashboard/due-dates?session=${currentCourseSession.id}`;
} else { } else {
return `/appointments`; return `/dashboard/due-dates`;
} }
}); });
@ -121,6 +122,8 @@ const hasLearningMentor = computed(() => {
const courseSession = courseSessionsStore.currentCourseSession; const courseSession = courseSessionsStore.currentCourseSession;
return courseSession.actions.includes("learning-mentor"); return courseSession.actions.includes("learning-mentor");
}); });
const mentorTabTitle = useMentorTexts().mentorTabTitle;
</script> </script>
<template> <template>
@ -265,7 +268,7 @@ const hasLearningMentor = computed(() => {
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': inLearningMentor() }" :class="{ 'nav-item--active': inLearningMentor() }"
> >
{{ t("a.Lernbegleitung") }} {{ t(mentorTabTitle) }}
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -10,6 +10,7 @@ import {
getLearningPathUrl, getLearningPathUrl,
getMediaCenterUrl, getMediaCenterUrl,
} from "@/utils/utils"; } from "@/utils/utils";
import { useMentorTexts } from "@/composables";
const router = useRouter(); const router = useRouter();
@ -36,6 +37,8 @@ const clickLink = (to: string | undefined) => {
emit("closemodal"); emit("closemodal");
} }
}; };
const mentorTabTitle = useMentorTexts().mentorTabTitle;
</script> </script>
<template> <template>
@ -97,7 +100,7 @@ const clickLink = (to: string | undefined) => {
data-cy="navigation-mobile-mentor-link" data-cy="navigation-mobile-mentor-link"
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))" @click="clickLink(getLearningMentorUrl(courseSession.course.slug))"
> >
{{ $t("a.Lernbegleitung") }} {{ $t(mentorTabTitle) }}
</button> </button>
</li> </li>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useMentorTexts } from "@/composables";
import ItModal from "@/components/ui/ItModal.vue"; import ItModal from "@/components/ui/ItModal.vue";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useCSRFFetch } from "@/fetchHelpers"; import { useCSRFFetch } from "@/fetchHelpers";
@ -68,19 +68,22 @@ const inviteMentor = async () => {
showInvitationModal.value = false; showInvitationModal.value = false;
inviteeEmail.value = ""; inviteeEmail.value = "";
}; };
const { myLearningMentors, inviteLearningMentor, noLearningMentors } = useMentorTexts();
</script> </script>
<template> <template>
<div v-if="!isLoading" class="bg-gray-200"> <div v-if="!isLoading" class="bg-gray-200">
<div class="flex flex-row items-center justify-between py-6"> <div class="flex flex-row items-center justify-between py-6">
<h2 class="heading-2">{{ $t("a.Meine Lernbegleitung") }}</h2> <h2 data-cy="lm-my-lms-title" class="heading-2">{{ $t(myLearningMentors) }}</h2>
<div> <div>
<button <button
class="btn-secondary flex items-center" class="btn-secondary flex items-center"
data-cy="lm-invite-mentor-button"
@click="showInvitationModal = true" @click="showInvitationModal = true"
> >
<it-icon-add class="it-icon mr-2 h-6 w-6" /> <it-icon-add class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Neue Lernbegleitung einladen") }} {{ $t(inviteLearningMentor) }}
</button> </button>
</div> </div>
</div> </div>
@ -143,16 +146,14 @@ const inviteMentor = async () => {
<div class="j mx-1 my-3 flex w-fit items-center space-x-1 bg-sky-200 p-4"> <div class="j mx-1 my-3 flex w-fit items-center space-x-1 bg-sky-200 p-4">
<it-icon-info class="it-icon mr-2 h-6 w-6 text-sky-700" /> <it-icon-info class="it-icon mr-2 h-6 w-6 text-sky-700" />
<span> <span>
{{ {{ $t(noLearningMentors) }}
$t("a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.")
}}
</span> </span>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
<ItModal v-model="showInvitationModal"> <ItModal v-model="showInvitationModal">
<template #title>{{ $t("a.Neue Lernbegleitung einladen") }}</template> <template #title>{{ $t(inviteLearningMentor) }}</template>
<template #body> <template #body>
<div class="flex flex-col"> <div class="flex flex-col">
<label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label> <label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label>

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useMentorTexts } from "@/composables";
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const { actionNoLearningMentors, inviteLearningMentorShort } = useMentorTexts();
</script> </script>
<template> <template>
@ -9,11 +10,7 @@ const currentCourseSession = useCurrentCourseSession();
<it-icon-info class="it-icon h-6 w-6 text-sky-700" /> <it-icon-info class="it-icon h-6 w-6 text-sky-700" />
<div> <div>
<div class="mb-4"> <div class="mb-4">
{{ {{ $t(actionNoLearningMentors) }}
$t(
"a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
)
}}
</div> </div>
<router-link <router-link
:to="{ :to="{
@ -22,7 +19,7 @@ const currentCourseSession = useCurrentCourseSession();
}" }"
class="btn-blue px-4 py-2 font-bold" class="btn-blue px-4 py-2 font-bold"
> >
{{ $t("a.Lernbegleitung einladen") }} {{ $t(inviteLearningMentorShort) }}
</router-link> </router-link>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables"; import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall"; export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
@ -46,14 +46,31 @@ const wrapperClasses = computed(() => {
} }
return classes; return classes;
}); });
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
lpQueryResult.circles
);
</script> </script>
<template> <template>
<div :class="wrapperClasses"> <div>
<LearningPathCircle <h4
v-for="circle in circles" v-if="diagramType === 'horizontal' && circles.length > 0"
:key="circle.id" class="mb-4 font-bold"
:sectors="calculateCircleSectorData(circle)" >
></LearningPathCircle> {{
$t("learningPathPage.progressText", {
inProgressCount: inProgressCirclesCount,
allCount: circlesCount,
})
}}
</h4>
<div :class="wrapperClasses">
<LearningPathCircle
v-for="circle in circles"
:key="circle.id"
:sectors="calculateCircleSectorData(circle)"
></LearningPathCircle>
</div>
</div> </div>
</template> </template>

View File

@ -21,7 +21,7 @@ const numFeedbacks = computed(() => {
}); });
onMounted(async () => { onMounted(async () => {
const data = await itGet( const data: { amount: number } = await itGet(
`/api/core/feedback/${props.courseSession.id}/${props.circleId}/` `/api/core/feedback/${props.courseSession.id}/${props.circleId}/`
); );
completeFeedbacks.value = data.amount; completeFeedbacks.value = data.amount;

View File

@ -28,104 +28,106 @@ const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
</script> </script>
<template> <template>
<template v-if="isLoaded"> <div>
<!-- Self Evaluation --> <template v-if="isLoaded">
<div class="bg-white px-8 py-4 lg:mb-8 lg:py-8"> <!-- Self Evaluation -->
<div class="mb-8"> <div class="bg-white px-8 py-4 lg:mb-8 lg:py-8">
<h3 class="mb-4 pb-4 lg:pb-0"> <div class="mb-8">
{{ $t("a.Selbsteinschätzungen") }} <h3 class="mb-4 pb-4 lg:pb-0">
</h3> {{ $t("a.Selbsteinschätzungen") }}
<ul </h3>
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8" <ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-fail"
>
{{ selfAssessmentCounts?.fail }}
</p>
</div>
</li>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-success"
>
{{ selfAssessmentCounts?.pass }}
</p>
</div>
</li>
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-unknown"
>
{{ selfAssessmentCounts?.unknown }}
</p>
</div>
</li>
</ul>
</div>
<!-- Feedback Evaluation -->
<div
v-if="courseSession.course.configuration.enable_learning_mentor"
class="border-t pt-8"
> >
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0"> <h3 class="mb-4 pb-4 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5> {{ $t("a.Fremdeinschätzungen") }}
<div class="flex flex-row items-center"> </h3>
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking> <ul
<p class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
class="ml-4 inline-block text-7xl font-bold" >
data-cy="self-evaluation-fail" <li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
> <h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5>
{{ selfAssessmentCounts?.fail }} <div class="flex flex-row items-center">
</p> <it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
</div> <p class="ml-4 inline-block text-7xl font-bold">
</li> {{ feedbackEvaluationCounts?.fail }}
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0"> </p>
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5> </div>
<div class="flex flex-row items-center"> </li>
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy> <li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<p <h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
class="ml-4 inline-block text-7xl font-bold" <div class="flex flex-row items-center">
data-cy="self-evaluation-success" <it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
> <p class="ml-4 inline-block text-7xl font-bold">
{{ selfAssessmentCounts?.pass }} {{ feedbackEvaluationCounts?.pass }}
</p> </p>
</div> </div>
</li> </li>
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0"> <li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5> <h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral> <it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p <p class="ml-4 inline-block text-7xl font-bold">
class="ml-4 inline-block text-7xl font-bold" {{ feedbackEvaluationCounts?.unknown }}
data-cy="self-evaluation-unknown" </p>
> </div>
{{ selfAssessmentCounts?.unknown }} </li>
</p> </ul>
</div> </div>
</li> <!-- Show All (always)-->
</ul> <button
</div> class="btn-text inline-flex items-center py-2 pl-0 pt-8"
<!-- Feedback Evaluation --> @click="emit('showAll')"
<div
v-if="courseSession.course.configuration.enable_learning_mentor"
class="border-t pt-8"
>
<h3 class="mb-4 pb-4 lg:pb-0">
{{ $t("a.Fremdeinschätzungen") }}
</h3>
<ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
> >
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0"> <span>{{ $t("general.showAll") }}</span>
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5> <it-icon-arrow-right></it-icon-arrow-right>
<div class="flex flex-row items-center"> </button>
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.fail }}
</p>
</div>
</li>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.pass }}
</p>
</div>
</li>
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.unknown }}
</p>
</div>
</li>
</ul>
</div> </div>
<!-- Show All (always)--> </template>
<button </div>
class="btn-text inline-flex items-center py-2 pl-0 pt-8"
@click="emit('showAll')"
>
<span>{{ $t("general.showAll") }}</span>
<it-icon-arrow-right></it-icon-arrow-right>
</button>
</div>
</template>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,12 +1,29 @@
import { useCSRFFetch } from "@/fetchHelpers"; import { useCSRFFetch } from "@/fetchHelpers";
import type { CourseStatisticsType } from "@/gql/graphql"; import type { CourseStatisticsType } from "@/gql/graphql";
import { graphqlClient } from "@/graphql/client"; import { graphqlClient } from "@/graphql/client";
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries"; import {
COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
COMPETENCE_NAVI_CERTIFICATE_QUERY,
COURSE_QUERY,
COURSE_SESSION_DETAIL_QUERY,
} from "@/graphql/queries";
import { import {
circleFlatChildren, circleFlatChildren,
circleFlatLearningContents, circleFlatLearningContents,
circleFlatLearningUnits, circleFlatLearningUnits,
someFinishedInLearningSequence,
} from "@/services/circle"; } from "@/services/circle";
import type {
DashboardDueDate,
DashboardPersonRoleType,
DashboardPersonType,
} from "@/services/dashboard";
import {
courseIdForCourseSlug,
fetchDashboardDueDates,
fetchDashboardPersons,
fetchStatisticData,
} from "@/services/dashboard";
import { presignUpload, uploadFile } from "@/services/files"; import { presignUpload, uploadFile } from "@/services/files";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
@ -14,11 +31,13 @@ import { useDashboardStore } from "@/stores/dashboard";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { import type {
ActionCompetence, ActionCompetence,
CircleType,
Course, Course,
CourseCompletion, CourseCompletion,
CourseCompletionStatus, CourseCompletionStatus,
CourseSession, CourseSession,
CourseSessionDetail, CourseSessionDetail,
DashboardPersonsPageMode,
LearningContentWithCompletion, LearningContentWithCompletion,
LearningMentor, LearningMentor,
LearningPathType, LearningPathType,
@ -26,9 +45,11 @@ import type {
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import { useQuery } from "@urql/vue"; import { useQuery } from "@urql/vue";
import dayjs from "dayjs";
import { t } from "i18next";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import log from "loglevel"; import log from "loglevel";
import type { ComputedRef } from "vue"; import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref, watchEffect } from "vue"; import { computed, onMounted, ref, watchEffect } from "vue";
export function useCurrentCourseSession() { export function useCurrentCourseSession() {
@ -166,7 +187,7 @@ export function useCourseData(courseSlug: string) {
log.error(result.error); log.error(result.error);
} }
course.value = result.data?.course as Course; course.value = result.data?.course as unknown as Course;
actionCompetences.value = result.data?.course actionCompetences.value = result.data?.course
?.action_competences as ActionCompetence[]; ?.action_competences as ActionCompetence[];
learningPath.value = result.data?.course?.learning_path as LearningPathType; learningPath.value = result.data?.course?.learning_path as LearningPathType;
@ -487,3 +508,254 @@ export function useMyLearningMentors() {
loading, loading,
}; };
} }
export function getVvRoleDisplay(role: DashboardPersonRoleType) {
switch (role) {
case "LEARNING_MENTOR":
return t("a.Lernbegleitung");
case "LEARNING_MENTEE":
return t("a.Teilnehmer");
case "EXPERT":
return t("a.Experte");
case "MEMBER":
return t("a.Teilnehmer");
case "SUPERVISOR":
return t("a.Regionenleiter");
default:
return role;
}
}
export function getUkRoleDisplay(role: DashboardPersonRoleType) {
switch (role) {
case "LEARNING_MENTOR":
return t("a.Praxisbildner");
case "LEARNING_MENTEE":
return t("a.Teilnehmer");
case "EXPERT":
return t("a.Trainer");
case "MEMBER":
return t("a.Teilnehmer");
case "SUPERVISOR":
return t("a.Regionenleiter");
default:
return role;
}
}
export function useDashboardPersonsDueDates(
mode: DashboardPersonsPageMode = "default"
) {
const dashboardPersons = ref<DashboardPersonType[]>([]);
const dashboardDueDates = ref<DashboardDueDate[]>([]);
const loading = ref(false);
// due dates from today to future
const currentDueDates = ref<DashboardDueDate[]>([]);
const fetchData = async () => {
loading.value = true;
try {
const [persons, dueDates] = await Promise.all([
fetchDashboardPersons(mode),
fetchDashboardDueDates(),
]);
dashboardPersons.value = persons;
// attach role name to persons
dashboardPersons.value.forEach((person) => {
person.course_sessions.forEach((cs) => {
if (cs.is_uk) {
cs.my_role_display = getUkRoleDisplay(cs.my_role);
cs.user_role_display = getUkRoleDisplay(cs.user_role);
} else if (cs.is_vv) {
cs.my_role_display = getVvRoleDisplay(cs.my_role);
cs.user_role_display = getVvRoleDisplay(cs.user_role);
} else {
cs.my_role_display = "";
cs.user_role_display = "";
}
});
});
dashboardDueDates.value = dueDates.map((dueDate) => {
const dateType = t(dueDate.date_type_translation_key);
const assignmentType = t(dueDate.assignment_type_translation_key);
dueDate.translatedType = dateType;
if (assignmentType) {
dueDate.translatedType += " " + assignmentType;
}
return dueDate;
});
currentDueDates.value = dashboardDueDates.value.filter((dueDate) => {
let refDate = dayjs(dueDate.start);
if (dueDate.end) {
refDate = dayjs(dueDate.end);
}
return refDate >= dayjs().startOf("day");
});
// attach `LEARNING_MENTEE` to due dates for `LEARNING_MENTOR` persons
currentDueDates.value.forEach((dueDate) => {
if (dueDate.course_session.my_role === "LEARNING_MENTOR") {
dueDate.persons = dashboardPersons.value.filter((person) => {
if (
person.course_sessions
.map((cs) => cs.id)
.includes(dueDate.course_session.id)
) {
return person.course_sessions.some(
(cs) => cs.user_role === "LEARNING_MENTEE"
);
}
});
}
});
} catch (error) {
console.error("Error fetching data:", error);
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return {
dashboardPersons,
dashboardDueDates,
currentDueDates,
loading,
};
}
export function useMentorTexts() {
const texts = computed(() => {
try {
const is_uk =
useCourseSessionsStore().currentCourseSession?.course.configuration.is_uk;
const mentorTabTitle = is_uk ? "a.Praxisbildner" : "a.Lernbegleitung";
const myLearningMentors = is_uk ? "Meine Praxisbildner" : "Meine Lernbegleiter";
const inviteLearningMentor = is_uk
? "Neuen Praxisbildner einladen"
: "a.Neue Lernbegleitung einladen";
const inviteLearningMentorShort = is_uk
? "Praxisbildner einladen"
: "Lernbegleitung einladen";
const noLearningMentors = is_uk
? "a.Aktuell hast du noch keine Person als Praxisbildner eingeladen."
: "a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.";
const actionNoLearningMentors = is_uk
? "a.Aktuell hast du noch keine Person als Praxisbildner eingeladen. Lade jetzt jemanden ein."
: "a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein.";
return {
mentorTabTitle,
myLearningMentors,
inviteLearningMentor,
inviteLearningMentorShort,
noLearningMentors,
actionNoLearningMentors,
};
} catch (e) {
return {
mentorTabTitle: "",
myLearningMentors: "",
inviteLearningMentor: "",
inviteLearningMentorShort: "",
noLearningMentors: "",
actionNoLearningMentors: "",
};
}
});
return { ...texts.value };
}
export function useCourseCircleProgress(circles: Ref<CircleType[] | undefined>) {
const inProgressCirclesCount = computed(() => {
if (circles.value?.length) {
return circles.value.filter(
(circle) =>
circle.learning_sequences.filter((ls) => someFinishedInLearningSequence(ls))
.length
).length;
}
return 0;
});
const circlesCount = computed(() => {
return circles.value?.length ?? 0;
});
return { inProgressCirclesCount, circlesCount };
}
export function useCourseStatisticsv2(courseSlug: string) {
const dashboardStore = useDashboardStore();
const courseStatistics = ref<CourseStatisticsType | null>(null);
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
await dashboardStore.loadDashboardDetails();
const courseId = courseIdForCourseSlug(
dashboardStore.dashboardConfigsv2,
courseSlug
);
try {
if (courseId) {
courseStatistics.value = await fetchStatisticData(courseId);
}
} finally {
loading.value = false;
}
};
const courseSessionName = (courseSessionId: string) => {
return courseStatistics?.value?.course_session_properties?.sessions.find(
(session) => session.id === courseSessionId
)?.name;
};
const circleMeta = (circleId: string) => {
return courseStatistics?.value?.course_session_properties.circles.find(
(circle) => circle.id === circleId
);
};
onMounted(fetchData);
return {
courseStatistics,
loading,
courseSessionName,
circleMeta,
};
}
export function useCertificateQuery(userId: string | undefined, courseSlug: string) {
const certificatesQuery = (() => {
const courseSession = useCurrentCourseSession();
if (userId) {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.value.id,
userId: userId,
},
});
} else {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.value.id,
},
});
}
})();
return { certificatesQuery };
}

View File

@ -20,7 +20,11 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => {
}); });
}; };
export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => { export const itPost = <T>(
url: RequestInfo,
data: unknown,
options: RequestInit = {}
) => {
options = Object.assign({}, options); options = Object.assign({}, options);
const headers = Object.assign( const headers = Object.assign(
@ -56,11 +60,11 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
return response.json().catch(() => { return response.json().catch(() => {
return Promise.resolve(null); return Promise.resolve(null);
}); });
}); }) as Promise<T>;
}; };
export const itGet = (url: RequestInfo) => { export const itGet = <T>(url: RequestInfo) => {
return itPost(url, {}, { method: "GET" }); return itPost<T>(url, {}, { method: "GET" });
}; };
export const itDelete = (url: RequestInfo) => { export const itDelete = (url: RequestInfo) => {
@ -81,17 +85,17 @@ export function bustItGetCache(key?: string) {
} }
} }
export const itGetCached = ( export const itGetCached = <T>(
url: RequestInfo, url: RequestInfo,
options = { options = {
reload: false, reload: false,
} }
): Promise<any> => { ): Promise<T> => {
if (!itGetPromiseCache.has(url.toString()) || options.reload) { if (!itGetPromiseCache.has(url.toString()) || options.reload) {
itGetPromiseCache.set(url.toString(), itGet(url)); itGetPromiseCache.set(url.toString(), itGet<T>(url));
} }
return itGetPromiseCache.get(url.toString()) as Promise<any>; return itGetPromiseCache.get(url.toString()) as Promise<T>;
}; };
export const useCSRFFetch = createFetch({ export const useCSRFFetch = createFetch({

View File

@ -19,11 +19,14 @@ const documents = {
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument, "\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument, "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument, "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateForUserQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument, "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument, "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n }\n": types.DashboardConfigDocument, "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n": types.DashboardConfigDocument,
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument, "\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument, "\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n": types.DashboardCourseDataDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
"\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n }\n }\n": types.MentorCourseStatisticsDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
}; };
@ -65,6 +68,10 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@ -72,11 +79,11 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId:
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n }\n"]; export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@ -84,7 +91,15 @@ export function graphql(source: "\n query dashboardProgress($courseId: ID!) {\n
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"): (typeof documents)["\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"]; export function graphql(source: "\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n"): (typeof documents)["\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"): (typeof documents)["\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
type Query { type Query {
course_statistics(course_id: ID!): CourseStatisticsType course_statistics(course_id: ID!): CourseStatisticsType
mentor_course_statistics(course_id: ID!): BaseStatisticsType
course_progress(course_id: ID!): CourseProgressType course_progress(course_id: ID!): CourseProgressType
dashboard_config: [DashboardConfigType!]! dashboard_config: [DashboardConfigType!]!
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
@ -20,6 +21,7 @@ type Query {
learning_content_document_list: LearningContentDocumentListObjectType learning_content_document_list: LearningContentDocumentListObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
competence_certificate_list_for_user(id: ID, slug: String, course_id: ID, course_slug: String, user_id: UUID): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType assignment(id: ID, slug: String): AssignmentObjectType
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
} }
@ -29,15 +31,59 @@ type CourseStatisticsType {
course_id: ID! course_id: ID!
course_title: String! course_title: String!
course_slug: String! course_slug: String!
course_session_properties: StatisticsCourseSessionPropertiesType!
course_session_selection_ids: [ID]! course_session_selection_ids: [ID]!
user_selection_ids: [ID]
assignments: AssignmentsStatisticsType!
course_session_properties: StatisticsCourseSessionPropertiesType!
course_session_selection_metrics: StatisticsCourseSessionsSelectionMetricType! course_session_selection_metrics: StatisticsCourseSessionsSelectionMetricType!
attendance_day_presences: AttendanceDayPresencesStatisticsType! attendance_day_presences: AttendanceDayPresencesStatisticsType!
feedback_responses: FeedbackStatisticsResponsesType! feedback_responses: FeedbackStatisticsResponsesType!
assignments: AssignmentsStatisticsType!
competences: CompetencesStatisticsType! competences: CompetencesStatisticsType!
} }
type AssignmentsStatisticsType {
_id: ID!
records: [AssignmentStatisticsRecordType!]!
summary: AssignmentStatisticsSummaryType!
}
type AssignmentStatisticsRecordType {
_id: ID!
course_session_id: ID!
course_session_assignment_id: ID!
circle_id: ID!
generation: String!
assignment_type_translation_key: String!
assignment_title: String!
deadline: DateTime!
metrics: AssignmentCompletionMetricsType!
details_url: String!
}
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime
type AssignmentCompletionMetricsType {
_id: ID!
passed_count: Int!
failed_count: Int!
unranked_count: Int!
ranking_completed: Boolean!
average_passed: Float!
}
type AssignmentStatisticsSummaryType {
_id: ID!
completed_count: Int!
average_passed: Float!
total_passed: Int!
total_failed: Int!
}
type StatisticsCourseSessionPropertiesType { type StatisticsCourseSessionPropertiesType {
_id: ID! _id: ID!
sessions: [StatisticsCourseSessionDataType!]! sessions: [StatisticsCourseSessionDataType!]!
@ -79,13 +125,6 @@ type PresenceRecordStatisticsType {
details_url: String! details_url: String!
} }
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime
type AttendanceSummaryStatisticsType { type AttendanceSummaryStatisticsType {
_id: ID! _id: ID!
days_completed: Int! days_completed: Int!
@ -116,40 +155,6 @@ type FeedbackStatisticsSummaryType {
total_responses: Int! total_responses: Int!
} }
type AssignmentsStatisticsType {
_id: ID!
records: [AssignmentStatisticsRecordType!]!
summary: AssignmentStatisticsSummaryType!
}
type AssignmentStatisticsRecordType {
_id: ID!
course_session_id: ID!
course_session_assignment_id: ID!
circle_id: ID!
generation: String!
assignment_type_translation_key: String!
assignment_title: String!
deadline: DateTime!
metrics: AssignmentCompletionMetricsType!
details_url: String!
}
type AssignmentCompletionMetricsType {
_id: ID!
passed_count: Int!
failed_count: Int!
unranked_count: Int!
ranking_completed: Boolean!
average_passed: Float!
}
type AssignmentStatisticsSummaryType {
_id: ID!
completed_count: Int!
average_passed: Float!
}
type CompetencesStatisticsType { type CompetencesStatisticsType {
_id: ID! _id: ID!
summary: CompetencePerformanceStatisticsSummaryType! summary: CompetencePerformanceStatisticsSummaryType!
@ -173,12 +178,22 @@ type CompetenceRecordStatisticsType {
details_url: String! details_url: String!
} }
type BaseStatisticsType {
_id: ID!
course_id: ID!
course_title: String!
course_slug: String!
course_session_selection_ids: [ID]!
user_selection_ids: [ID]
assignments: AssignmentsStatisticsType!
}
type CourseProgressType { type CourseProgressType {
_id: ID! _id: ID!
course_id: ID! course_id: ID!
session_to_continue_id: ID session_to_continue_id: ID
competence: ProgressDashboardCompetenceType! competence: ProgressDashboardCompetenceType
assignment: ProgressDashboardAssignmentType! assignment: ProgressDashboardAssignmentType
} }
type ProgressDashboardCompetenceType { type ProgressDashboardCompetenceType {
@ -208,6 +223,7 @@ enum DashboardType {
PROGRESS_DASHBOARD PROGRESS_DASHBOARD
SIMPLE_DASHBOARD SIMPLE_DASHBOARD
MENTOR_DASHBOARD MENTOR_DASHBOARD
PRAXISBILDNER_DASHBOARD
} }
type CourseConfigurationObjectType { type CourseConfigurationObjectType {
@ -215,6 +231,8 @@ type CourseConfigurationObjectType {
enable_circle_documents: Boolean! enable_circle_documents: Boolean!
enable_learning_mentor: Boolean! enable_learning_mentor: Boolean!
enable_competence_certificates: Boolean! enable_competence_certificates: Boolean!
is_vv: Boolean!
is_uk: Boolean!
} }
type LearningPathObjectType implements CoursePageInterface { type LearningPathObjectType implements CoursePageInterface {

View File

@ -15,6 +15,7 @@ export const AttendanceSummaryStatisticsType = "AttendanceSummaryStatisticsType"
export const AttendanceUserInputType = "AttendanceUserInputType"; export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType"; export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus"; export const AttendanceUserStatus = "AttendanceUserStatus";
export const BaseStatisticsType = "BaseStatisticsType";
export const Boolean = "Boolean"; export const Boolean = "Boolean";
export const CircleLightObjectType = "CircleLightObjectType"; export const CircleLightObjectType = "CircleLightObjectType";
export const CircleObjectType = "CircleObjectType"; export const CircleObjectType = "CircleObjectType";

View File

@ -117,6 +117,42 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
} }
`); `);
export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
query competenceCertificateForUserQuery(
$courseSlug: String!
$courseSessionId: ID!
$userId: UUID!
) {
competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {
...CoursePageFields
competence_certificates {
...CoursePageFields
assignments {
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
}
learning_content {
...CoursePageFields
circle {
id
title
slug
}
}
}
}
}
}
`);
export const COURSE_SESSION_DETAIL_QUERY = graphql(` export const COURSE_SESSION_DETAIL_QUERY = graphql(`
query courseSessionDetail($courseSessionId: ID!) { query courseSessionDetail($courseSessionId: ID!) {
course_session(id: $courseSessionId) { course_session(id: $courseSessionId) {
@ -220,6 +256,7 @@ export const COURSE_QUERY = graphql(`
enable_circle_documents enable_circle_documents
enable_learning_mentor enable_learning_mentor
enable_competence_certificates enable_competence_certificates
is_uk
} }
action_competences { action_competences {
competence_id competence_id
@ -304,6 +341,7 @@ export const DASHBOARD_CONFIG = graphql(`
enable_circle_documents enable_circle_documents
enable_learning_mentor enable_learning_mentor
enable_competence_certificates enable_competence_certificates
is_uk
} }
} }
} }
@ -331,6 +369,16 @@ export const DASHBOARD_COURSE_SESSION_PROGRESS = graphql(`
} }
`); `);
export const DASHBOARD_COURSE_DATA = graphql(`
query dashboardCourseData($courseId: ID!) {
course_progress(course_id: $courseId) {
_id
course_id
session_to_continue_id
}
}
`);
export const DASHBOARD_COURSE_STATISTICS = graphql(` export const DASHBOARD_COURSE_STATISTICS = graphql(`
query courseStatistics($courseId: ID!) { query courseStatistics($courseId: ID!) {
course_statistics(course_id: $courseId) { course_statistics(course_id: $courseId) {
@ -400,6 +448,8 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id _id
completed_count completed_count
average_passed average_passed
total_passed
total_failed
} }
records { records {
_id _id
@ -442,3 +492,45 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
} }
} }
`); `);
export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
query mentorCourseStatistics($courseId: ID!) {
mentor_course_statistics(course_id: $courseId) {
_id
course_id
course_title
course_slug
course_session_selection_ids
user_selection_ids
assignments {
_id
summary {
_id
completed_count
average_passed
total_passed
total_failed
}
records {
_id
course_session_id
course_session_assignment_id
circle_id
generation
assignment_title
assignment_type_translation_key
details_url
deadline
metrics {
_id
passed_count
failed_count
unranked_count
ranking_completed
average_passed
}
}
}
}
}
`);

View File

@ -1,179 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useTranslation } from "i18next-vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import type { DueDate } from "@/types";
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseData } from "@/composables";
const { t } = useTranslation();
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
const courseSessionsStore = useCourseSessionsStore();
type Item = {
id: string;
name: string;
};
type CourseItem = Item & {
slug: string;
};
const courses: CourseItem[] = courseSessionsStore.uniqueCourseSessionsByCourse.map(
(cs) => ({
id: cs.course.id,
name: cs.course.title,
slug: cs.course.slug,
})
);
const selectedCourse = ref<CourseItem>(courses[0]);
const courseSessions = computed(() => {
return [
{
id: UNFILTERED,
name: t("a.AlleDurchführungen"),
},
...courseSessionsStore.allCourseSessions
.filter((cs) => cs.course.id === selectedCourse.value.id)
.map((cs) => ({ id: cs.id, name: cs.title })),
];
});
const selectedSession = ref<Item>(courseSessions.value[0]);
// pre-select course and session if we are in a course session
if (courseSessionsStore.currentCourseSession) {
const session = courseSessionsStore.currentCourseSession;
const { id: courseId, title: courseName, slug: courseSlug } = session.course;
selectedCourse.value = { id: courseId, name: courseName, slug: courseSlug };
const { id: sessionId, title: sessionName } = session;
selectedSession.value = { id: sessionId, name: sessionName };
}
const initialItemCircle: Item = {
id: UNFILTERED,
name: t("a.AlleCircle"),
};
const circles = ref<Item[]>([initialItemCircle]);
const selectedCircle = ref<Item>(circles.value[0]);
async function loadCircleValues() {
if (selectedCourse.value) {
const learningPathQuery = useCourseData(selectedCourse.value.slug);
await learningPathQuery.resultPromise;
circles.value = [
initialItemCircle,
...(learningPathQuery.circles.value ?? []).map((circle) => ({
id: circle.id,
name: circle.title,
})),
];
} else {
circles.value = [initialItemCircle];
}
selectedCircle.value = circles.value[0];
}
watch(selectedCourse, async () => {
selectedSession.value = courseSessions.value[0];
await loadCircleValues();
});
onMounted(async () => {
await loadCircleValues();
});
const appointments = computed(() => {
return courseSessionsStore
.allDueDates()
.filter(
(dueDate) =>
isMatchingCourse(dueDate) &&
isMatchingSession(dueDate) &&
isMatchingCircle(dueDate)
);
});
const isMatchingSession = (dueDate: DueDate) =>
selectedSession.value.id === UNFILTERED ||
dueDate.course_session_id === selectedSession.value.id;
const isMatchingCircle = (dueDate: DueDate) =>
selectedCircle.value.id === UNFILTERED ||
dueDate.circle?.id === selectedCircle.value.id;
const isMatchingCourse = (dueDate: DueDate) =>
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
const numAppointmentsToShow = ref(7);
const canLoadMore = computed(() => {
return numAppointmentsToShow.value < appointments.value.length;
});
async function loadAdditionalAppointments() {
numAppointmentsToShow.value *= 2;
}
</script>
<template>
<div class="bg-gray-200">
<div class="container-large px-8 py-8">
<header class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1>{{ $t("a.AlleTermine") }}</h1>
<div>
<ItDropdownSelect
v-model="selectedCourse"
data-cy="appointments-course-select"
:items="courses"
></ItDropdownSelect>
</div>
</header>
<main>
<div class="flex flex-col space-y-2">
<div class="flex flex-col space-x-0 bg-white lg:flex-row lg:space-x-3">
<ItDropdownSelect
v-model="selectedSession"
data-cy="appointments-session-select"
:items="courseSessions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="selectedCircle"
data-cy="appointments-circle-select"
:items="circles"
borderless
></ItDropdownSelect>
</div>
<div class="bg-white px-5">
<DueDatesList
:show-top-border="false"
:show-bottom-border="canLoadMore"
:due-dates="appointments"
:show-all-due-dates-link="false"
:max-count="numAppointmentsToShow"
data-cy="appointments-list"
:show-course-session="true"
/>
<button
v-if="canLoadMore"
class="py-4 underline"
data-cy="load-more-notifications"
@click="loadAdditionalAppointments()"
>
{{ $t("notifications.load_more") }}
</button>
</div>
</div>
</main>
</div>
</div>
</template>
<style lang="postcss" scoped>
.no-border-last li:last-child {
border-bottom: none !important;
}
</style>

View File

@ -41,8 +41,8 @@ const userStore = useUserStore();
class="bg-white p-4 lg:p-8" class="bg-white p-4 lg:p-8"
@submit.prevent=" @submit.prevent="
userStore.handleLogin( userStore.handleLogin(
state.username, state.username.trim(),
state.password, state.password.trim(),
route.query.next as string route.query.next as string
) )
" "

View File

@ -40,7 +40,7 @@ const updateItems = async (_items: []) => {
}; };
onMounted(async () => { onMounted(async () => {
const response = await itGet("/api/notify/email_notification_settings/"); const response: any = await itGet("/api/notify/email_notification_settings/");
items.value = items.value.map((item) => { items.value = items.value.map((item) => {
item.checked = response.includes(item.value); item.checked = response.includes(item.value);
return item; return item;

View File

@ -73,7 +73,7 @@ function findUserPointsHtml(userId: string) {
"%)"; "%)";
if (!gradedUser.passed) { if (!gradedUser.passed) {
result += ` <span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5">${t( result += ` <span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5 inline-block leading-5">${t(
"a.Nicht bestanden" "a.Nicht bestanden"
)}</span>`; )}</span>`;
} }
@ -157,10 +157,11 @@ function findUserPointsHtml(userId: string) {
</div> </div>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <p
v-if="findGradedUser(csu.user_id) && !isPraxisAssignment" v-if="findGradedUser(csu.user_id) && !isPraxisAssignment"
class="text-left md:text-right"
v-html="findUserPointsHtml(csu.user_id)" v-html="findUserPointsHtml(csu.user_id)"
></div> ></p>
</section> </section>
</template> </template>
<template #link> <template #link>

View File

@ -6,10 +6,10 @@ import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composab
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue"; import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
import { useExpertCockpitStore } from "@/stores/expertCockpit"; import { useExpertCockpitStore } from "@/stores/expertCockpit";
import log from "loglevel"; import log from "loglevel";
import CockpitDates from "@/components/cockpit/CockpitDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue"; import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables"; import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -105,7 +105,11 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</div> </div>
<div class="mb-4 bg-white p-6"> <div class="mb-4 bg-white p-6">
<CockpitDates></CockpitDates> <CourseSessionDueDatesList
:course-session-id="courseSession.id"
:circle-id="expertCockpitStore.currentCircle.id"
:max-count="4"
></CourseSessionDueDatesList>
</div> </div>
<SubmissionsOverview <SubmissionsOverview
:course-session="courseSession" :course-session="courseSession"

View File

@ -35,7 +35,7 @@ const circleDocumentsResultData = ref<CircleDocument[]>([]);
let courseSessionDocumentsUrl = ""; let courseSessionDocumentsUrl = "";
async function fetchDocuments() { async function fetchDocuments() {
const result = await fetchCourseSessionDocuments(courseSession.value?.id); const result: any = await fetchCourseSessionDocuments(courseSession.value?.id);
if (result.length > 0) { if (result.length > 0) {
circleDocumentsResultData.value = result; circleDocumentsResultData.value = result;
} else { } else {

View File

@ -15,6 +15,7 @@ log.debug("CompetenceCertificateComponent setup");
const props = defineProps<{ const props = defineProps<{
competenceCertificate: CompetenceCertificate; competenceCertificate: CompetenceCertificate;
detailView: boolean; detailView: boolean;
frontendUrl?: string;
}>(); }>();
const totalPointsEvaluatedAssignments = computed(() => { const totalPointsEvaluatedAssignments = computed(() => {
@ -40,6 +41,12 @@ const progressStatusCount = computed(() => {
props.competenceCertificate.assignments props.competenceCertificate.assignments
); );
}); });
const frontendUrl = computed(() => {
return props.frontendUrl
? props.frontendUrl
: props.competenceCertificate.frontend_url;
});
</script> </script>
<template> <template>
@ -90,7 +97,7 @@ const progressStatusCount = computed(() => {
<div v-if="!props.detailView"> <div v-if="!props.detailView">
<router-link <router-link
:to="competenceCertificate.frontend_url" :to="frontendUrl"
class="btn-text mt-4 inline-flex items-center py-2 pl-0" class="btn-text mt-4 inline-flex items-center py-2 pl-0"
:data-cy="`certificate-${competenceCertificate.slug}-detail-link`" :data-cy="`certificate-${competenceCertificate.slug}-detail-link`"
> >

View File

@ -1,33 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import log from "loglevel"; import log from "loglevel";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import type { CompetenceCertificate } from "@/types"; import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables"; import { useCertificateQuery } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue"; import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { getCertificates } from "@/services/competence";
import { getPreviousRoute } from "@/router/history";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
certificateSlug: string; certificateSlug: string;
userId?: string;
}>(); }>();
log.debug("CompetenceCertificateDetailPage setup", props); log.debug("CompetenceCertificateDetailPage setup", props);
const courseSession = useCurrentCourseSession(); const certificatesQuery = useCertificateQuery(
props.userId,
const certificatesQuery = useQuery({ props.courseSlug
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, ).certificatesQuery;
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
},
});
const certificate = computed(() => { const certificate = computed(() => {
const certificates = getCertificates(
certificatesQuery.data.value,
props.userId ?? null
);
if (!certificates) {
return null;
}
return ( return (
(certificatesQuery.data.value?.competence_certificate_list (certificates.competence_certificates as unknown as CompetenceCertificate[]) ?? []
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
).find((cc) => cc.slug.endsWith(props.certificateSlug)); ).find((cc) => cc.slug.endsWith(props.certificateSlug));
}); });
@ -40,11 +44,14 @@ onMounted(async () => {
<div class="container-large"> <div class="container-large">
<nav class="py-4"> <nav class="py-4">
<router-link <router-link
class="btn-text inline-flex items-center pl-0" :to="
:to="`/course/${props.courseSlug}/competence/certificates`" getPreviousRoute() || `/course/${props.courseSlug}/competence/certificates`
"
class="btn-text inline-flex items-center p-0"
data-cy="back-button"
> >
<it-icon-arrow-left /> <it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
<span>{{ $t("general.back") }}</span> <span class="inline">{{ $t("general.back") }}</span>
</router-link> </router-link>
</nav> </nav>
<div v-if="certificate"> <div v-if="certificate">

View File

@ -1,64 +1,81 @@
<script setup lang="ts"> <script setup lang="ts">
import log from "loglevel"; import log from "loglevel";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import type { CompetenceCertificate } from "@/types"; import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables"; import { useCertificateQuery } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue"; import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { import {
assignmentsMaxEvaluationPoints, assignmentsMaxEvaluationPoints,
assignmentsUserPoints, assignmentsUserPoints,
} from "@/pages/competence/utils"; } from "@/pages/competence/utils";
import { useRoute } from "vue-router";
import { getCertificates } from "@/services/competence";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
userId?: string;
}>(); }>();
log.debug("CompetenceCertificateListPage setup", props); log.debug("CompetenceCertificateListPage setup", props);
const courseSession = useCurrentCourseSession(); const route = useRoute();
const certificatesQuery = useQuery({ const certificatesQuery = useCertificateQuery(
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, props.userId,
variables: { props.courseSlug
courseSlug: props.courseSlug, ).certificatesQuery;
courseSessionId: courseSession.value.id,
},
});
const competenceCertificates = computed(() => { const competenceCertificates = computed(() => {
const certificates = getCertificates(
certificatesQuery.data.value,
props.userId ?? null
);
if (!certificates) {
return null;
}
return ( return (
(certificatesQuery.data.value?.competence_certificate_list (certificates?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
); );
}); });
const assignments = computed(() => { const assignments = computed(() => {
return competenceCertificates.value.flatMap((cc) => cc.assignments); return competenceCertificates?.value?.flatMap((cc) => cc.assignments);
}); });
const totalPointsEvaluatedAssignments = computed(() => { const totalPointsEvaluatedAssignments = computed(() => {
return assignmentsMaxEvaluationPoints(assignments.value); return assignmentsMaxEvaluationPoints(assignments.value ?? []);
}); });
const userPointsEvaluatedAssignments = computed(() => { const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(assignments.value); return assignmentsUserPoints(assignments.value ?? []);
}); });
const numAssignmentsEvaluated = computed(() => { const numAssignmentsEvaluated = computed(() => {
return assignments.value.filter((a) => { return (assignments.value ?? []).filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED"; return a.completion?.completion_status === "EVALUATION_SUBMITTED";
}).length; }).length;
}); });
const certificateFrontendUrl = function (frontendUrl: string) {
if (props.userId) {
const pathSegments = frontendUrl.split("/");
const lastSegment = pathSegments[pathSegments.length - 1];
// Assuming you want to navigate to the current path + last segment
return `${route.path}/${lastSegment}`;
}
return frontendUrl;
};
onMounted(async () => { onMounted(async () => {
// log.debug("AssignmentView mounted", props.assignmentId, props.userId); // log.debug("AssignmentView mounted", props.assignmentId, props.userId);
}); });
</script> </script>
<template> <template>
<div class="container-large"> <div v-if="assignments" class="container-large">
<h2 class="mb-4 lg:py-4">{{ $t("a.Kompetenznachweise") }}</h2> <h2 class="mb-4 lg:py-4">{{ $t("a.Kompetenznachweise") }}</h2>
<div class="mb-4 bg-white p-8" data-cy="certificate-total-points-text"> <div class="mb-4 bg-white p-8" data-cy="certificate-total-points-text">
@ -92,6 +109,7 @@ onMounted(async () => {
<CompetenceCertificateComponent <CompetenceCertificateComponent
:competence-certificate="competenceCertificate" :competence-certificate="competenceCertificate"
:detail-view="false" :detail-view="false"
:frontend-url="certificateFrontendUrl(competenceCertificate.frontend_url)"
></CompetenceCertificateComponent> ></CompetenceCertificateComponent>
</div> </div>
</div> </div>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import log from "loglevel";
import { useDashboardPersonsDueDates } from "@/composables";
import { computed } from "vue";
import _ from "lodash";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
log.debug("DashboardAsideWidget created");
const { loading, dashboardPersons, dashboardDueDates } = useDashboardPersonsDueDates();
const displayDueDates = computed(() => {
return _.take(dashboardDueDates.value, 6);
});
</script>
<template>
<div>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else>
<section class="border-b p-8">
<h3 class="mb-4">{{ dashboardPersons.length }} {{ $t("a.Personen") }}</h3>
<div>
<router-link class="btn-secondary" to="/dashboard/persons">
{{ $t("a.Alle Personen anzeigen") }}
</router-link>
</div>
</section>
<section v-if="dashboardDueDates.length > 0" class="p-8">
<h3>{{ $t("a.Termine") }}</h3>
<div v-for="dueDate in displayDueDates" :key="dueDate.id">
<div class="border-b">
<DueDateSingle :due-date="dueDate" />
</div>
</div>
<router-link class="btn-secondary mt-4" to="/dashboard/due-dates">
{{ $t("a.Alle Termine anzeigen") }}
</router-link>
</section>
</div>
</div>
</template>

View File

@ -0,0 +1,268 @@
<script setup lang="ts">
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import log from "loglevel";
import { useDashboardPersonsDueDates } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, ref, watch } from "vue";
import { useTranslation } from "i18next-vue";
import _ from "lodash";
import { useRouteQuery } from "@vueuse/router";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import { getPreviousRoute } from "@/router/history";
log.debug("DashboardPersonsPage created");
const UNFILTERED = "0";
type DropboxItem = {
id: string;
name: string;
};
type CourseItem = DropboxItem & {
slug: string;
};
const { t } = useTranslation();
const { loading, currentDueDates: dashboardDueDates } = useDashboardPersonsDueDates();
const courses = computed(() => {
return [
{
id: UNFILTERED,
name: `${t("Lehrgang")}: ${t("a.Alle")}`,
slug: "",
},
..._(dashboardDueDates.value)
.map((dueDate) => {
return {
name: dueDate.course_session.course_title,
id: dueDate.course_session.course_id,
slug: dueDate.course_session.course_slug,
};
})
.uniqBy("id")
.orderBy("name")
.value(),
];
});
const selectedCourse = ref<CourseItem>(courses.value[0]);
const courseSessions = computed(() => {
let sessions = _(dashboardDueDates.value)
.map((dueDate) => {
return Object.assign({}, dueDate.course_session, {
name: dueDate.course_session.session_title,
id: dueDate.course_session.id,
});
})
.uniqBy("id")
.orderBy("name")
.value();
// filter by selected course
if (selectedCourse.value.id !== UNFILTERED) {
sessions = sessions.filter((cs) => cs.course_id === selectedCourse.value.id);
}
return [
{
id: UNFILTERED,
name: `${t("Durchführung")}: ${t("a.Alle")}`,
},
...sessions,
];
});
const selectedSessionRouteQuery = useRouteQuery("session", UNFILTERED, {
mode: "replace",
});
const selectedSession = ref<DropboxItem>(courseSessions.value[0]);
watch(selectedSession, () => {
// @ts-ignore
selectedSessionRouteQuery.value = selectedSession.value.id;
});
watch(courseSessions, () => {
if (courseSessions.value.length > 0 && selectedSessionRouteQuery.value) {
// preselect session from route query
const selectedSessionFromRoute = courseSessions.value.find(
(cs) => cs.id === selectedSessionRouteQuery.value
);
if (selectedSessionFromRoute) {
selectedSession.value = selectedSessionFromRoute;
return;
}
}
selectedSession.value = courseSessions.value[0];
});
const filteredDueDates = computed(() => {
return _.orderBy(
dashboardDueDates.value
.filter((dueDate) => {
if (selectedCourse.value.id === UNFILTERED) {
return true;
}
return dueDate.course_session.course_id === selectedCourse.value.id;
})
.filter((dueDate) => {
if (selectedSession.value.id === UNFILTERED) {
return true;
}
return dueDate.course_session.id === selectedSession.value.id;
})
.filter((dueDate) => {
if (selectedCircle.value.id === UNFILTERED) {
return true;
}
return dueDate.circle?.id === selectedCircle.value.id;
})
.filter((dueDate) => {
if (selectedType.value.id === UNFILTERED) {
return true;
}
return dueDate.translatedType === selectedType.value.id;
}),
["start"]
);
});
const filtersVisible = computed(() => {
return (
courses.value.length > 2 ||
courseSessions.value.length > 2 ||
circles.value.length > 2 ||
dueDateTypes.value.length > 2
);
});
const circles = computed(() => {
const dueDatesBySelectedCourse = dashboardDueDates.value.filter((dueDate) => {
if (selectedCourse.value.id === UNFILTERED) {
return true;
}
return dueDate.course_session.course_id === selectedCourse.value.id;
});
const circleList = _(dueDatesBySelectedCourse)
.filter((dueDate) => {
return !!dueDate.circle;
})
.map((dueDate) => {
return {
name: dueDate.circle?.title ?? "",
id: dueDate.circle?.id ?? "",
};
})
.uniqBy("id")
.orderBy("name")
.value();
return [
{
id: UNFILTERED,
name: `${t("Circle")}: ${t("a.Alle")}`,
},
...circleList,
];
});
const selectedCircle = ref<DropboxItem>(circles.value[0]);
const dueDateTypes = computed(() => {
const types = _(dashboardDueDates.value)
.map((dueDate) => {
return {
name: dueDate.translatedType,
id: dueDate.translatedType,
};
})
.uniqBy("id")
.orderBy("name")
.value();
return [
{
id: UNFILTERED,
name: t("a.AlleTypen"),
},
...types,
];
});
const selectedType = ref<DropboxItem>(dueDateTypes.value[0]);
watch(selectedCourse, async () => {
selectedSession.value = courseSessions.value[0];
selectedCircle.value = circles.value[0];
selectedType.value = dueDateTypes.value[0];
});
</script>
<template>
<div>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="bg-gray-200">
<div class="container-large">
<router-link
:to="getPreviousRoute() || '/'"
class="btn-text inline-flex items-center p-0"
data-cy="back-button"
>
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
<span class="inline">{{ $t("general.back") }}</span>
</router-link>
<h2 class="my-4">{{ $t("a.Termine") }}</h2>
<div class="bg-white px-4 py-2">
<section
v-if="filtersVisible"
class="flex flex-col space-x-0 border-b bg-white lg:flex-row lg:space-x-3"
>
<ItDropdownSelect
v-model="selectedCourse"
data-cy="select-course"
:items="courses"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="selectedSession"
data-cy="select-session"
:items="courseSessions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="selectedCircle"
data-cy="select-circle"
:items="circles"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="selectedType"
data-cy="select-type"
:items="dueDateTypes"
borderless
></ItDropdownSelect>
</section>
<section data-cy="due-date-list">
<div v-for="dueDate in filteredDueDates" :key="dueDate.id" class="border-b">
<DueDateSingle :single-line="true" :due-date="dueDate"></DueDateSingle>
</div>
<div v-if="!filteredDueDates.length" class="mt-4">
<p>{{ $t("dueDates.noDueDatesAvailable") }}</p>
</div>
</section>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,33 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Component } from "vue";
import { onMounted } from "vue"; import { onMounted } from "vue";
import StatisticPage from "@/pages/dashboard/StatisticPage.vue";
import ProgressPage from "@/pages/dashboard/ProgressPage.vue";
import SimpleDates from "@/components/dashboard/SimpleDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useDashboardStore } from "@/stores/dashboard"; import { useDashboardStore } from "@/stores/dashboard";
import type { DashboardType } from "@/gql/graphql"; import type { DashboardCourseConfigType } from "@/services/dashboard";
import SimpleCoursePage from "@/pages/dashboard/SimpleCoursePage.vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue"; import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import CourseDetailDates from "@/components/dashboard/CourseDetailDates.vue";
import NoCourseSession from "@/components/dashboard/NoCourseSession.vue"; import NoCourseSession from "@/components/dashboard/NoCourseSession.vue";
import MentorPage from "@/pages/dashboard/MentorPage.vue"; import CoursePanel from "@/components/dashboard/CoursePanel.vue";
import DashboardAsideWidget from "@/pages/dashboard/DashboardAsideWidget.vue";
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
interface DashboardPage { onMounted(async () => {
main: Component; await dashboardStore.loadDashboardDetails();
aside: Component; });
function newDashboardConfigForId(id: string): DashboardCourseConfigType | undefined {
return dashboardStore.dashboardConfigsv2.find((config) => config.course_id == id);
} }
const boards: Record<DashboardType, DashboardPage> = {
PROGRESS_DASHBOARD: { main: ProgressPage, aside: SimpleDates },
SIMPLE_DASHBOARD: { main: SimpleCoursePage, aside: SimpleDates },
STATISTICS_DASHBOARD: { main: StatisticPage, aside: CourseDetailDates },
MENTOR_DASHBOARD: { main: MentorPage, aside: SimpleDates },
};
onMounted(dashboardStore.loadDashboardDetails);
</script> </script>
<template> <template>
@ -35,30 +23,24 @@ onMounted(dashboardStore.loadDashboardDetails);
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
<div <div
v-else-if="dashboardStore.currentDashboardConfig" v-else-if="dashboardStore.dashboardConfigsv2.length"
class="flex flex-col lg:flex-row" class="flex flex-col lg:flex-row"
> >
<main class="grow bg-gray-200 lg:order-2"> <main class="grow bg-gray-200 lg:order-2">
<div class="m-8"> <div class="m-8">
<div class="mb-10 flex items-center justify-between"> <!-- new way of dashboard -->
<h1 data-cy="dashboard-title">Dashboard</h1> <ul>
<ItDropdownSelect <li
:model-value="dashboardStore.currentDashboardConfig" v-for="config in dashboardStore.dashboardConfigsv2"
class="mt-4 w-full lg:mt-0 lg:w-96" :key="config.course_id"
:items="dashboardStore.dashboardConfigs" >
@update:model-value="dashboardStore.switchAndLoadDashboardConfig" <CoursePanel :course-config="newDashboardConfigForId(config.course_id)" />
></ItDropdownSelect> </li>
</div> </ul>
<component
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main"
></component>
</div> </div>
</main> </main>
<aside class="m-8 lg:order-1 lg:w-[343px]"> <aside class="lg:order-2 lg:w-[384px] xl:w-[512px]">
<component <DashboardAsideWidget />
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].aside"
></component>
</aside> </aside>
</div> </div>
<NoCourseSession v-else class="container-medium mt-14" /> <NoCourseSession v-else class="container-medium mt-14" />

View File

@ -0,0 +1,394 @@
<script setup lang="ts">
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import log from "loglevel";
import { useDashboardPersonsDueDates } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, ref, watch } from "vue";
import { useTranslation } from "i18next-vue";
import _ from "lodash";
import type { DashboardPersonCourseSessionType } from "@/services/dashboard";
import { useRouteQuery } from "@vueuse/router";
import type { DashboardPersonsPageMode } from "@/types";
log.debug("DashboardPersonsPage created");
export interface Props {
mode?: DashboardPersonsPageMode;
}
const props = withDefaults(defineProps<Props>(), {
mode: "default",
});
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
type MenuItem = {
id: string;
name: string;
};
const { t } = useTranslation();
const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode);
const courses = computed(() => {
return [
{
id: UNFILTERED,
name: `${t("Lehrgang")}: ${t("a.Alle")}`,
},
..._(dashboardPersons.value)
.flatMap((person) => person.course_sessions)
.map((cs) => {
return { name: cs.course_title, id: cs.course_id };
})
.uniqBy("id")
.orderBy("name")
.value(),
];
});
const selectedCourse = ref<MenuItem>(courses.value[0]);
const selectedCourseRouteQuery = useRouteQuery("course", UNFILTERED, {
mode: "replace",
});
watch(selectedCourse, () => {
selectedCourseRouteQuery.value = selectedCourse.value.id;
});
watch(courses, () => {
if (selectedCourseRouteQuery.value !== UNFILTERED) {
selectedCourse.value =
courses.value.find((course) => course.id === selectedCourseRouteQuery.value) ||
courses.value[0];
}
});
const regions = computed(() => {
let values = _(dashboardPersons.value)
.flatMap((person) => person.course_sessions)
.map((cs) => {
return Object.assign({}, cs, { name: cs.region, id: cs.region });
})
.filter((cs) => !!cs.region)
.uniqBy("id")
.orderBy("name")
.value();
// filter by selected course
if (selectedCourse.value.id !== UNFILTERED) {
values = values.filter((cs) => cs.course_id === selectedCourse.value.id);
}
return [
{
id: UNFILTERED,
name: `${t("Region")}: ${t("a.Alle")}`,
},
...values,
];
});
const selectedRegion = ref<MenuItem>(regions.value[0]);
const courseSessions = computed(() => {
let values = _(dashboardPersons.value)
.flatMap((person) => person.course_sessions)
.map((cs) => {
return Object.assign({}, cs, { name: cs.session_title, id: cs.id });
})
.uniqBy("id")
.orderBy("name")
.value();
// filter by selected course
if (selectedCourse.value.id !== UNFILTERED) {
values = values.filter((cs) => cs.course_id === selectedCourse.value.id);
}
// filter by selected region
if (selectedRegion.value.id !== UNFILTERED) {
values = values.filter((cs) => cs.region === selectedRegion.value.id);
}
return [
{
id: UNFILTERED,
name: `${t("Durchführung")}: ${t("a.Alle")}`,
},
...values,
];
});
const selectedSession = ref<MenuItem>(courseSessions.value[0]);
const generations = computed(() => {
const values = _(dashboardPersons.value)
.flatMap((person) => person.course_sessions)
.map((cs) => {
return Object.assign({}, cs, { name: cs.generation, id: cs.generation });
})
.filter((cs) => !!cs.generation)
.uniqBy("id")
.orderBy("name")
.value();
return [
{
id: UNFILTERED,
name: `${t("Generation")}: ${t("a.Alle")}`,
},
...values,
];
});
const selectedGeneration = ref<MenuItem>(generations.value[0]);
const roles = computed(() => {
const values = _(dashboardPersons.value)
.flatMap((person) => person.course_sessions)
.map((cs) => {
return Object.assign({}, cs, {
name: cs.user_role_display,
id: cs.user_role_display,
});
})
.uniqBy("id")
.orderBy("name")
.value();
return [
{
id: "",
name: `${t("Rolle")}: ${t("a.Alle")}`,
},
...values,
];
});
const selectedRole = ref<MenuItem>(roles.value[0]);
const filteredPersons = computed(() => {
return _.orderBy(
dashboardPersons.value
.filter((person) => {
if (selectedCourse.value.id === UNFILTERED) {
return true;
}
return person.course_sessions.some(
(cs) => cs.course_id === selectedCourse.value.id
);
})
.filter((person) => {
if (selectedSession.value.id === UNFILTERED) {
return true;
}
return person.course_sessions.some((cs) => cs.id === selectedSession.value.id);
})
.filter((person) => {
if (selectedRegion.value.id === UNFILTERED) {
return true;
}
return person.course_sessions.some(
(cs) => cs.region === selectedRegion.value.id
);
})
.filter((person) => {
if (selectedGeneration.value.id === UNFILTERED) {
return true;
}
return person.course_sessions.some(
(cs) => cs.generation === selectedGeneration.value.id
);
})
.filter((person) => {
if (selectedRole.value.id === "") {
return true;
}
return person.course_sessions.some(
(cs) => cs.user_role_display === selectedRole.value.id
);
}),
["last_name", "first_name"]
);
});
const filtersVisible = computed(() => {
return (
courses.value.length > 2 ||
courseSessions.value.length > 2 ||
regions.value.length > 2 ||
generations.value.length > 2 ||
roles.value.length > 2
);
});
function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessionType) {
if (
["SUPERVISOR", "EXPERT", "LEARNING_MENTOR"].includes(personCourseSession.user_role)
) {
return personCourseSession.user_role_display;
}
return "";
}
watch(selectedCourse, () => {
selectedRegion.value = regions.value[0];
});
watch(selectedRegion, () => {
selectedSession.value = courseSessions.value[0];
selectedGeneration.value = generations.value[0];
selectedRole.value = roles.value[0];
});
</script>
<template>
<div>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="bg-gray-200">
<div class="container-large">
<router-link
:to="`/`"
class="btn-text inline-flex items-center p-0"
data-cy="back-to-learning-path-button"
>
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
<span class="inline">{{ $t("general.back") }}</span>
</router-link>
<h2 class="my-4">{{ $t("a.Personen") }}</h2>
<div class="bg-white px-4 py-2">
<section
v-if="filtersVisible"
class="flex flex-col space-x-0 border-b bg-white lg:flex-row lg:space-x-3"
>
<ItDropdownSelect
v-if="courses.length > 2"
v-model="selectedCourse"
data-cy="select-course"
:items="courses"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-if="regions.length > 2"
v-model="selectedRegion"
data-cy="select-region"
:items="regions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-if="courseSessions.length > 2"
v-model="selectedSession"
data-cy="select-course"
:items="courseSessions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-if="generations.length > 2"
v-model="selectedGeneration"
data-cy="select-generation"
:items="generations"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-if="roles.length > 2"
v-model="selectedRole"
data-cy="select-role"
:items="roles"
borderless
></ItDropdownSelect>
</section>
<div
v-for="person in filteredPersons"
:key="person.user_id"
data-cy="person"
class="flex flex-col justify-between gap-4 border-b p-2 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="w-full flex-auto md:w-1/3">
<div class="flex items-center space-x-2">
<img
class="inline-block h-11 w-11 rounded-full"
:src="
person.avatar_url_small ||
'/static/avatars/myvbv-default-avatar.png'
"
:alt="`${person.first_name} ${person.last_name}`"
/>
<div>
<div class="text-bold">
{{ person.first_name }}
{{ person.last_name }}
</div>
<div class="text-gray-900">{{ person.email }}</div>
</div>
</div>
</div>
<div class="w-full flex-auto items-start md:w-2/3">
<div
v-for="cs in person.course_sessions"
:key="cs.id"
class="w-full border-b pb-2 pt-2 first:pt-0 last:border-b-0 last:pb-0"
>
<div class="flex flex-col md:flex-row md:items-center">
<div v-if="props.mode === 'default'" class="md:w-1/2">
<div class="text-gray-900">{{ cs.course_title }}</div>
<div v-if="cs.is_uk">{{ cs.session_title }}</div>
</div>
<div v-if="props.mode === 'competenceMetrics'" class="md:w-1/3">
<div
v-if="cs.competence_metrics?.passed_count || 0 > 0"
class="my-2 w-fit rounded-md bg-green-200 px-2.5 py-0.5"
>
{{ $t("a.Bestanden") }}:
{{ cs.competence_metrics?.passed_count }}
</div>
</div>
<div v-if="props.mode === 'default'" class="md:w-1/4">
{{ personRoleDisplayValue(cs) }}
</div>
<div v-else-if="props.mode === 'competenceMetrics'" class="md:w-1/3">
<div
v-if="cs.competence_metrics?.failed_count || 0 > 0"
class="my-2 w-fit rounded-md bg-error-red-200 px-2.5 py-0.5"
>
{{ $t("a.Nicht Bestanden") }}:
{{ cs.competence_metrics?.failed_count }}
</div>
</div>
<div class="md:w-1/4 md:text-right">
<div
v-if="
(['SUPERVISOR', 'EXPERT'].includes(cs.my_role) &&
cs.user_role === 'MEMBER') ||
(cs.my_role === 'LEARNING_MENTOR' &&
cs.user_role === 'LEARNING_MENTEE')
"
>
<router-link
:to="{
name: 'profileLearningPath',
params: {
userId: person.user_id,
courseSlug: cs.course_slug,
},
query: { courseSessionId: cs.id },
}"
class="link w-full lg:text-right"
>
{{ $t("a.Profil anzeigen") }}
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,25 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type { import type {
AssignmentCompletionMetricsType, AssignmentCompletionMetricsType,
AssignmentStatisticsRecordType, AssignmentStatisticsRecordType,
CourseStatisticsType, CourseStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { useCourseStatistics } from "@/composables";
import ItProgress from "@/components/ui/ItProgress.vue";
import { getDateString } from "@/components/dueDates/dueDatesUtils"; import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ItProgress from "@/components/ui/ItProgress.vue";
const dashboardStore = useDashboardStore(); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
const statistics = computed(() => { courseStatistics: CourseStatisticsType;
return dashboardStore.currentDashBoardData as CourseStatisticsType; courseSessionName: (sessionId: string) => string;
}); circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const { courseSessionName, circleMeta } = useCourseStatistics();
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => { const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
if (!metrics.ranking_completed) { if (!metrics.ranking_completed) {
@ -43,20 +39,14 @@ const total = (metrics: AssignmentCompletionMetricsType) => {
</script> </script>
<template> <template>
<main v-if="statistics"> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3> <h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3>
<ItDropdownSelect
:model-value="dashboardStore.currentDashboardConfig"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="dashboardStore.dashboardConfigs"
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div> </div>
<div v-if="statistics.assignments.records" class="mt-8 bg-white"> <div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white">
<StatisticFilterList <StatisticFilterList
:course-session-properties="statistics.course_session_properties" :course-session-properties="courseStatistics?.course_session_properties"
:items="statistics.assignments.records" :items="courseStatistics.assignments.records"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="flex justify-between"> <div class="flex justify-between">
@ -89,8 +79,8 @@ const total = (metrics: AssignmentCompletionMetricsType) => {
<div v-else>Noch nicht bestätigt</div> <div v-else>Noch nicht bestätigt</div>
<ItProgress <ItProgress
:status-count=" :status-count="
assignmentStats((item as AssignmentStatisticsRecordType).metrics) assignmentStats((item as AssignmentStatisticsRecordType).metrics)
" "
></ItProgress> ></ItProgress>
<router-link <router-link
class="underline" class="underline"

View File

@ -1,21 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard"; import type {
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; CourseStatisticsType,
import { computed } from "vue"; PresenceRecordStatisticsType,
import type { CourseStatisticsType, PresenceRecordStatisticsType } from "@/gql/graphql"; StatisticsCircleDataType,
} from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { useCourseStatistics } from "@/composables";
import { getDateString } from "@/components/dueDates/dueDatesUtils"; import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs"; import dayjs from "dayjs";
const dashboardStore = useDashboardStore(); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
const statistics = computed(() => { courseStatistics: CourseStatisticsType;
return dashboardStore.currentDashBoardData as CourseStatisticsType; courseSessionName: (sessionId: string) => string;
}); circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const { courseSessionName, circleMeta } = useCourseStatistics();
const attendanceStats = (present: number, total: number) => { const attendanceStats = (present: number, total: number) => {
return { return {
@ -27,20 +26,17 @@ const attendanceStats = (present: number, total: number) => {
</script> </script>
<template> <template>
<main v-if="statistics"> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("Anwesenheit") }}</h3> <h3>{{ $t("Anwesenheit") }}</h3>
<ItDropdownSelect
:model-value="dashboardStore.currentDashboardConfig"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="dashboardStore.dashboardConfigs"
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div> </div>
<div v-if="statistics.attendance_day_presences.records" class="mt-8 bg-white"> <div
v-if="courseStatistics?.attendance_day_presences.records"
class="mt-8 bg-white"
>
<StatisticFilterList <StatisticFilterList
:course-session-properties="statistics.course_session_properties" :course-session-properties="courseStatistics.course_session_properties"
:items="statistics.attendance_day_presences.records" :items="courseStatistics.attendance_day_presences.records"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="flex justify-between"> <div class="flex justify-between">

View File

@ -1,38 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type { import type {
CompetenceRecordStatisticsType, CompetenceRecordStatisticsType,
CourseStatisticsType, CourseStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { useCourseStatistics } from "@/composables";
const dashboardStore = useDashboardStore(); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
const statistics = computed(() => { courseStatistics: CourseStatisticsType;
return dashboardStore.currentDashBoardData as CourseStatisticsType; courseSessionName: (sessionId: string) => string;
}); circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const { courseSessionName, circleMeta } = useCourseStatistics();
</script> </script>
<template> <template>
<main v-if="statistics"> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Selbsteinschätzung") }}</h3> <h3>{{ $t("a.Selbsteinschätzung") }}</h3>
<ItDropdownSelect
:model-value="dashboardStore.currentDashboardConfig"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="dashboardStore.dashboardConfigs"
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div> </div>
<div v-if="statistics.competences.records" class="mt-8 bg-white"> <div v-if="courseStatistics?.competences.records" class="mt-8 bg-white">
<StatisticFilterList <StatisticFilterList
:course-session-properties="statistics.course_session_properties" :course-session-properties="courseStatistics.course_session_properties"
:items="statistics.competences.records" :items="courseStatistics.competences.records"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="flex justify-between"> <div class="flex justify-between">

View File

@ -1,40 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type { import type {
CourseStatisticsType, CourseStatisticsType,
FeedbackStatisticsRecordType, FeedbackStatisticsRecordType,
PresenceRecordStatisticsType, PresenceRecordStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { useCourseStatistics } from "@/composables";
import { getBlendedColorForRating } from "@/utils/ratingToColor"; import { getBlendedColorForRating } from "@/utils/ratingToColor";
const dashboardStore = useDashboardStore(); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
const statistics = computed(() => { courseStatistics: CourseStatisticsType;
return dashboardStore.currentDashBoardData as CourseStatisticsType; courseSessionName: (sessionId: string) => string;
}); circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const { courseSessionName, circleMeta } = useCourseStatistics();
</script> </script>
<template> <template>
<main v-if="statistics"> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Feedback Teilnehmer") }}</h3> <h3>{{ $t("a.Feedback Teilnehmer") }}</h3>
<ItDropdownSelect
:model-value="dashboardStore.currentDashboardConfig"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="dashboardStore.dashboardConfigs"
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div> </div>
<div v-if="statistics.feedback_responses.records" class="mt-8 bg-white"> <div v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white">
<StatisticFilterList <StatisticFilterList
:course-session-properties="statistics.course_session_properties" :course-session-properties="courseStatistics.course_session_properties"
:items="statistics.feedback_responses.records" :items="courseStatistics.feedback_responses.records"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="flex justify-between"> <div class="flex justify-between">

View File

@ -1,15 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import { onMounted } from "vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue"; import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import { useCourseStatisticsv2 } from "@/composables";
const dashboardStore = useDashboardStore(); const props = defineProps<{
onMounted(dashboardStore.loadDashboardDetails); courseSlug: string;
}>();
const { courseStatistics, loading, courseSessionName, circleMeta } =
useCourseStatisticsv2(props.courseSlug);
</script> </script>
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<div v-if="dashboardStore.loading" class="m-8 flex justify-center"> <div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
<div v-else class="container-large flex flex-col space-y-8"> <div v-else class="container-large flex flex-col space-y-8">
@ -17,7 +20,11 @@ onMounted(dashboardStore.loadDashboardDetails);
<it-icon-arrow-left /> <it-icon-arrow-left />
<span>{{ $t("general.back") }}</span> <span>{{ $t("general.back") }}</span>
</router-link> </router-link>
<router-view></router-view> <router-view
:course-statistics="courseStatistics"
:course-session-name="courseSessionName"
:circle-meta="circleMeta"
></router-view>
</div> </div>
</div> </div>
</template> </template>

View File

@ -29,7 +29,19 @@ onMounted(() => {
> >
<ul class="flex flex-col lg:flex-row"> <ul class="flex flex-col lg:flex-row">
<li <li
data-cy="lm-mentees-navigation-link"
class="border-t-2 border-t-transparent" class="border-t-2 border-t-transparent"
:class="{
'border-b-2 border-b-blue-900': route.name === 'mentorsAndParticipants',
}"
>
<router-link :to="{ name: 'mentorsAndParticipants' }" class="block py-3">
{{ $t("a.Personen") }}
</router-link>
</li>
<li
v-if="!courseSession.course.configuration.is_uk"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ :class="{
'border-b-2 border-b-blue-900': route.name 'border-b-2 border-b-blue-900': route.name
?.toString() ?.toString()
@ -41,19 +53,7 @@ onMounted(() => {
:to="{ name: 'learningMentorOverview' }" :to="{ name: 'learningMentorOverview' }"
class="block py-3" class="block py-3"
> >
{{ $t("a.Übersicht") }} {{ $t("a.Aufgaben") }}
</router-link>
</li>
<li
data-cy="lm-mentees-navigation-link"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{
'border-b-2 border-b-blue-900': route.name === 'mentorsAndParticipants',
}"
>
<router-link :to="{ name: 'mentorsAndParticipants' }" class="block py-3">
{{ $t("a.Personen") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>

View File

@ -35,7 +35,7 @@ const courseSession = useCurrentCourseSession();
const circleDocumentsResultData = ref<CircleDocument[]>([]); const circleDocumentsResultData = ref<CircleDocument[]>([]);
async function fetchDocuments() { async function fetchDocuments() {
const result = await fetchCourseSessionDocuments(courseSession.value?.id); const result: any = await fetchCourseSessionDocuments(courseSession.value?.id);
if (result.length > 0) { if (result.length > 0) {
circleDocumentsResultData.value = result; circleDocumentsResultData.value = result;
} else { } else {

View File

@ -22,7 +22,7 @@ const documents = ref<BlockDocument[]>([]);
onMounted(async () => { onMounted(async () => {
log.debug("DocumentListBlock mounted"); log.debug("DocumentListBlock mounted");
const response = await itGetCached(`/api/course/page/${props.content.slug}/`); const response: any = await itGetCached(`/api/course/page/${props.content.slug}/`);
documents.value = response.documents; documents.value = response.documents;
}); });
</script> </script>

View File

@ -64,7 +64,7 @@ async function startTest() {
extendedTimeTest.value = true; extendedTimeTest.value = true;
} }
const response = await itPost("/api/core/edoniq-test/redirect/", { const response: any = await itPost("/api/core/edoniq-test/redirect/", {
learning_content_id: props.content.id, learning_content_id: props.content.id,
extended_time_test: extendedTimeTest.value, extended_time_test: extendedTimeTest.value,
}); });

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import DueDatesShortList from "@/components/dueDates/DueDatesShortList.vue";
import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue"; import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue"; import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
@ -8,8 +7,12 @@ import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPat
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue"; import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useCourseDataWithCompletion } from "@/composables"; import {
import { someFinishedInLearningSequence } from "@/services/circle"; useCourseCircleProgress,
useCourseDataWithCompletion,
useCurrentCourseSession,
} from "@/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -28,20 +31,11 @@ const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
const learningPath = computed(() => lpQueryResult.learningPath.value); const learningPath = computed(() => lpQueryResult.learningPath.value);
const course = computed(() => lpQueryResult.course.value); const course = computed(() => lpQueryResult.course.value);
const circlesCount = computed(() => { const courseSession = useCurrentCourseSession();
return lpQueryResult.circles.value?.length ?? 0;
});
const inProgressCirclesCount = computed(() => { const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
if (lpQueryResult.circles.value?.length) { lpQueryResult.circles
return lpQueryResult.circles.value.filter( );
(circle) =>
circle.learning_sequences.filter((ls) => someFinishedInLearningSequence(ls))
.length
).length;
}
return 0;
});
const changeViewType = (viewType: ViewType) => { const changeViewType = (viewType: ViewType) => {
selectedView.value = viewType; selectedView.value = viewType;
@ -72,10 +66,10 @@ const changeViewType = (viewType: ViewType) => {
<!-- Right --> <!-- Right -->
<div v-if="!useMobileLayout" class="flex-grow"> <div v-if="!useMobileLayout" class="flex-grow">
<div class="text-bold pb-3"> <CourseSessionDueDatesList
{{ $t("learningPathPage.nextDueDates") }} :course-session-id="courseSession.id"
</div> :max-count="2"
<DueDatesShortList :max-count="2" :show-top-border="true"></DueDatesShortList> ></CourseSessionDueDatesList>
</div> </div>
</div> </div>

View File

@ -202,7 +202,7 @@ const executePayment = () => {
redirect_url: fullHost, redirect_url: fullHost,
address: address.value, address: address.value,
product: props.courseType, product: props.courseType,
}).then((res) => { }).then((res: any) => {
console.log("Going to next page", res.next_step_url); console.log("Going to next page", res.next_step_url);
window.location.href = res.next_step_url; window.location.href = res.next_step_url;
}); });

View File

@ -1,63 +1,102 @@
<script setup lang="ts"> <script setup lang="ts">
import CockpitProfileContent from "@/components/userProfile/UserProfileContent.vue"; import CockpitProfileContent from "@/components/userProfile/UserProfileContent.vue";
import { ref } from "vue";
import SelfEvaluationAndFeedbackList from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue";
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { useRoute } from "vue-router";
type SubMenuType = "OVERVIEW" | "DETAILS";
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
courseSlug: string; courseSlug: string;
certificateSlug?: string;
}>(); }>();
interface SubMenuItem { interface SubMenuItem {
type: SubMenuType;
label: string; label: string;
url: string;
inMenu: boolean;
routeMatch: string[];
} }
const MENU_ENTRIES: SubMenuItem[] = [ const SUBPAGES: SubMenuItem[] = [
{ type: "OVERVIEW", label: "a.Übersicht" },
{ {
type: "DETAILS", label: "a.Übersicht",
label: useCurrentCourseSession().value.course.configuration.enable_learning_mentor url: `/course/${props.courseSlug}/profile/${props.userId}/competence`,
inMenu: true,
routeMatch: ["competenceMain"],
},
{
label: useCurrentCourseSession().value.course.configuration.is_vv
? "a.Selbst- und Fremdeinschätzungen" ? "a.Selbst- und Fremdeinschätzungen"
: "a.Selbsteinschätzungen", : "a.Selbsteinschätzungen",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/evaluations`,
inMenu: true,
routeMatch: ["competenceEvaluations"],
}, },
]; ];
const active = ref<SubMenuItem>(MENU_ENTRIES[0]); if (useCurrentCourseSession().value.course.configuration.is_uk) {
const selectDetails = () => { SUBPAGES.push(
active.value = MENU_ENTRIES[1]; {
}; label: "Kompetenznachweise",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/certificates`,
inMenu: true,
routeMatch: ["competenceCertificates", "competenceCertificateDetail"],
},
{
label: "",
url: "",
inMenu: false,
routeMatch: [],
}
);
}
function convertRouteRecordNameToString(
routeRecordName: string | symbol | undefined
): string {
if (!routeRecordName) {
return "";
}
if (typeof routeRecordName === "symbol") {
// Convert symbol to string explicitly
return routeRecordName.toString();
} else {
// It's already a string, return as is
return routeRecordName;
}
}
const route = useRoute();
</script> </script>
<template> <template>
<CockpitProfileContent> <CockpitProfileContent>
<template #side> <template #side>
<div v-for="(entry, index) in MENU_ENTRIES" :key="index" class="mb-2"> <div
<button v-for="(entry, index) in SUBPAGES.filter((p) => p.inMenu)"
:key="index"
class="mb-2"
>
<router-link
:to="entry.url"
class="flex w-full items-center space-x-2 p-2 pr-4 text-blue-900 hover:bg-gray-200 lg:pr-8" class="flex w-full items-center space-x-2 p-2 pr-4 text-blue-900 hover:bg-gray-200 lg:pr-8"
:class="{ 'text-bold bg-gray-200': active.type === entry.type }" :class="{
@click="active = entry" 'text-bold bg-gray-200': route.matched.some((record) =>
entry.routeMatch.includes(convertRouteRecordNameToString(record?.name))
),
}"
> >
<span>{{ $t(entry.label) }}</span> <span>{{ $t(entry.label) }}</span>
</button> </router-link>
</div> </div>
</template> </template>
<template #main> <template #main>
<div class="container-large"> <div class="container-large">
<SelfEvaluationAndFeedbackOverview <router-view
v-if="active.type === 'OVERVIEW'"
:profile-user-id="props.userId" :profile-user-id="props.userId"
@show-all="selectDetails" :user-id="props.userId"
/> :course-slug="useCurrentCourseSession().value.course.slug"
<SelfEvaluationAndFeedbackList :certificate-slug="certificateSlug ? certificateSlug : ''"
v-else-if="active.type === 'DETAILS'" ></router-view>
class="w-full"
:profile-user-id="props.userId"
/>
</div> </div>
</template> </template>
</CockpitProfileContent> </CockpitProfileContent>

View File

@ -13,8 +13,16 @@ const props = defineProps<{
const { t } = useTranslation(); const { t } = useTranslation();
const pages = ref([ const pages = ref([
{ label: t("general.learningPath"), route: "profileLearningPath" }, {
{ label: t("a.KompetenzNavi"), route: "profileCompetence" }, label: t("general.learningPath"),
route: "profileLearningPath",
routeMatch: "profileLearningPath",
},
{
label: t("a.KompetenzNavi"),
route: "competenceMain",
routeMatch: "profileCompetence",
},
]); ]);
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
@ -54,7 +62,11 @@ onMounted(() => {
v-for="page in pages" v-for="page in pages"
:key="page.route" :key="page.route"
class="relative top-px mr-12 pb-3" class="relative top-px mr-12 pb-3"
:class="[route.name === page.route ? 'border-b-2 border-blue-900 pb-3' : '']" :class="[
route.matched.some((record) => record.name === page.routeMatch)
? 'border-b-2 border-blue-900 pb-3'
: '',
]"
> >
<router-link :to="{ name: page.route }"> <router-link :to="{ name: page.route }">
{{ page.label }} {{ page.label }}

View File

@ -51,7 +51,10 @@ const loginRequired = (to: RouteLocationNormalized) => {
return !to.meta?.public; return !to.meta?.public;
}; };
export async function handleCurrentCourseSession(to: RouteLocationNormalized) { export async function handleCurrentCourseSession(
to: RouteLocationNormalized,
options?: { unset?: boolean }
) {
// register after login hooks // register after login hooks
const userStore = useUserStore(); const userStore = useUserStore();
if (userStore.loggedIn) { if (userStore.loggedIn) {
@ -59,9 +62,12 @@ export async function handleCurrentCourseSession(to: RouteLocationNormalized) {
if (to.params.courseSlug) { if (to.params.courseSlug) {
courseSessionsStore._currentCourseSlug = to.params.courseSlug as string; courseSessionsStore._currentCourseSlug = to.params.courseSlug as string;
} else { } else {
courseSessionsStore._currentCourseSlug = ""; if (options?.unset) {
courseSessionsStore._currentCourseSlug = "";
}
} }
if (!courseSessionsStore.loaded) { if (!courseSessionsStore.loaded) {
console.log("handleCurrentCourseSession: loadCourseSessionsData");
await courseSessionsStore.loadCourseSessionsData(); await courseSessionsStore.loadCourseSessionsData();
} }
} }
@ -77,6 +83,7 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
if (userStore.loggedIn) { if (userStore.loggedIn) {
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
if (!courseSessionsStore.loaded) { if (!courseSessionsStore.loaded) {
console.log("handleCourseSessionAsQueryParam: loadCourseSessionsData");
await courseSessionsStore.loadCourseSessionsData(); await courseSessionsStore.loadCourseSessionsData();
} }
@ -89,7 +96,7 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
return { return {
path: to.path, path: to.path,
query: restOfQuery, query: restOfQuery,
replace: true, // replace: true,
}; };
} else { } else {
// courseSessionId is invalid for current user -> redirect to home // courseSessionId is invalid for current user -> redirect to home

View File

@ -7,19 +7,32 @@ import type {
} from "vue-router"; } from "vue-router";
const routeHistory: RouteLocationNormalized[] = []; const routeHistory: RouteLocationNormalized[] = [];
const MAX_HISTORY = 10; // for example, store the last 10 visited routes const MAX_HISTORY = 10;
let isFirstNavigation = true; let isFirstNavigation = true;
let lastNavigationWasPush = false;
export function setLastNavigationWasPush(value: boolean) {
lastNavigationWasPush = value;
}
export function getLastNavigationWasPush() {
return lastNavigationWasPush;
}
export const addToHistory: NavigationGuard = (to, from, next) => { export const addToHistory: NavigationGuard = (to, from, next) => {
// Add the current route to the history, and ensure it doesn't exceed the maximum length // Add the current route to the history, and ensure it doesn't exceed the maximum length
if (isFirstNavigation) { if (isFirstNavigation) {
isFirstNavigation = false; isFirstNavigation = false;
} else { } else if (lastNavigationWasPush) {
routeHistory.push(from); routeHistory.push(from);
} }
if (routeHistory.length > MAX_HISTORY) { if (routeHistory.length > MAX_HISTORY) {
routeHistory.shift(); routeHistory.shift();
} }
lastNavigationWasPush = false;
next(); next();
}; };

View File

@ -10,7 +10,7 @@ import {
redirectToLoginIfRequired, redirectToLoginIfRequired,
updateLoggedIn, updateLoggedIn,
} from "@/router/guards"; } from "@/router/guards";
import { addToHistory } from "@/router/history"; import { addToHistory, setLastNavigationWasPush } from "@/router/history";
import { onboardingRedirect } from "@/router/onboarding"; import { onboardingRedirect } from "@/router/onboarding";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
@ -60,6 +60,19 @@ const router = createRouter({
name: "home", name: "home",
component: DashboardPage, component: DashboardPage,
}, },
{
path: "/dashboard/persons",
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
},
{
path: "/dashboard/persons-competence",
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
props: { mode: "competenceMetrics" },
},
{
path: "/dashboard/due-dates",
component: () => import("@/pages/dashboard/DashboardDueDatesPage.vue"),
},
{ {
path: "/course/:courseSlug/media", path: "/course/:courseSlug/media",
props: true, props: true,
@ -166,6 +179,37 @@ const router = createRouter({
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"), component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
props: true, props: true,
name: "profileCompetence", name: "profileCompetence",
children: [
{
path: "",
name: "competenceMain",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue"
),
},
{
path: "evaluations",
name: "competenceEvaluations",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue"
),
},
{
path: "certificates/:certificateSlug",
name: "competenceCertificateDetail",
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
path: "certificates",
name: "competenceCertificates",
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
},
],
}, },
], ],
}, },
@ -177,16 +221,16 @@ const router = createRouter({
children: [ children: [
{ {
path: "", path: "",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "participants",
component: () => component: () =>
import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"), import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"),
name: "mentorsAndParticipants", name: "mentorsAndParticipants",
}, },
{
path: "tasks",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{ {
path: "self-evaluation-feedback/:learningUnitId", path: "self-evaluation-feedback/:learningUnitId",
component: () => component: () =>
@ -264,7 +308,7 @@ const router = createRouter({
], ],
}, },
{ {
path: "/statistic", path: "/statistic/:courseSlug",
props: true, props: true,
component: () => import("@/pages/dashboard/statistic/StatisticParentPage.vue"), component: () => import("@/pages/dashboard/statistic/StatisticParentPage.vue"),
children: [ children: [
@ -316,14 +360,6 @@ const router = createRouter({
path: "/notifications", path: "/notifications",
component: () => import("@/pages/NotificationsPage.vue"), component: () => import("@/pages/NotificationsPage.vue"),
}, },
{
path: "/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{
path: "/course/:courseSlug/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{ {
path: "/onboarding/:courseType", path: "/onboarding/:courseType",
props: true, props: true,
@ -387,9 +423,26 @@ router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired); router.beforeEach(redirectToLoginIfRequired);
// register after login hooks // register after login hooks
router.beforeEach(handleCurrentCourseSession); router.beforeEach(async (to) => await handleCurrentCourseSession(to));
router.beforeEach(handleCourseSessionAsQueryParam); router.beforeEach(async (to) => await handleCourseSessionAsQueryParam(to));
// only unset the current course session in the after hook
router.afterEach(async (to) => await handleCurrentCourseSession(to, { unset: true }));
router.beforeEach(addToHistory); router.beforeEach(addToHistory);
// Wrap router.replace to track when it's called
const originalReplace = router.replace;
router.replace = function (to) {
setLastNavigationWasPush(false);
return originalReplace.call(this, to);
};
// Wrap router.push to track when it's called
const originalPush = router.push;
router.push = function (to) {
setLastNavigationWasPush(true);
return originalPush.call(this, to);
};
export default router; export default router;

View File

@ -1,3 +1,8 @@
import type {
CompetenceCertificateForUserQueryQuery,
CompetenceCertificateListObjectType,
CompetenceCertificateQueryQuery,
} from "@/gql/graphql";
import type { PerformanceCriteria } from "@/types"; import type { PerformanceCriteria } from "@/types";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
@ -17,3 +22,42 @@ export function calcPerformanceCriteriaStatusCount(criteria: PerformanceCriteria
FAIL: 0, FAIL: 0,
}; };
} }
// Type guards
export function isCompetenceCertificateForUserQueryQuery(
data: any
): data is CompetenceCertificateForUserQueryQuery {
return (
(data as CompetenceCertificateForUserQueryQuery)
.competence_certificate_list_for_user !== undefined
);
}
export function isCompetenceCertificateQueryQuery(
data: any
): data is CompetenceCertificateQueryQuery {
return (
(data as CompetenceCertificateQueryQuery).competence_certificate_list !== undefined
);
}
export function getCertificates(
data: any,
userId: string | null
): CompetenceCertificateListObjectType | null {
if (!data) {
return null;
}
let certificates = null;
if (userId && isCompetenceCertificateForUserQueryQuery(data)) {
certificates = data.competence_certificate_list_for_user;
} else if (isCompetenceCertificateQueryQuery(data)) {
certificates = data.competence_certificate_list;
} else {
// Handle case where data does not match expected types
console.error("Data structure is not recognized!");
return null;
}
return (certificates as unknown as CompetenceCertificateListObjectType) ?? null;
}

View File

@ -3,13 +3,93 @@ import {
DASHBOARD_CONFIG, DASHBOARD_CONFIG,
DASHBOARD_COURSE_SESSION_PROGRESS, DASHBOARD_COURSE_SESSION_PROGRESS,
DASHBOARD_COURSE_STATISTICS, DASHBOARD_COURSE_STATISTICS,
DASHBOARD_MENTOR_COMPETENCE_SUMMARY,
} from "@/graphql/queries"; } from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import type { import type {
AssignmentsStatisticsType,
CourseProgressType, CourseProgressType,
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import type { DashboardPersonsPageMode, DueDate } from "@/types";
export type DashboardPersonRoleType =
| "SUPERVISOR"
| "EXPERT"
| "MEMBER"
| "LEARNING_MENTOR"
| "LEARNING_MENTEE";
export type DashboardRoleKeyType =
| "Supervisor"
| "Expert"
| "Member"
| "MentorUK"
| "MentorVV";
export type WidgetType =
| "ProgressWidget"
| "CompetenceWidget"
| "MentorTasksWidget"
| "MentorPersonWidget"
| "MentorCompetenceWidget"
| "CompetenceCertificateWidget"
| "UKStatisticsWidget";
export type DashboardPersonCourseSessionType = {
id: string;
session_title: string;
course_id: string;
course_title: string;
course_slug: string;
region: string;
generation: string;
user_role: DashboardPersonRoleType;
user_role_display: string;
my_role: DashboardPersonRoleType;
my_role_display: string;
is_uk: boolean;
is_vv: boolean;
competence_metrics?: {
passed_count: number;
failed_count: number;
};
};
export type DashboardPersonType = {
user_id: string;
first_name: string;
last_name: string;
email: string;
course_sessions: DashboardPersonCourseSessionType[];
avatar_url: string;
avatar_url_small: string;
competence_metrics?: {
passed_count: number;
failed_count: number;
};
};
export type DashboardCourseConfigType = {
course_id: string;
course_slug: string;
course_title: string;
role_key: DashboardRoleKeyType;
is_uk: boolean;
is_vv: boolean;
is_mentor: boolean;
widgets: WidgetType[];
has_preview: boolean;
session_to_continue_id: string;
};
export type DashboardDueDate = DueDate & {
course_session: DashboardPersonCourseSessionType;
translatedType: string;
persons?: DashboardPersonType[];
};
export const fetchStatisticData = async ( export const fetchStatisticData = async (
courseId: string courseId: string
@ -47,6 +127,7 @@ export const fetchProgressData = async (
return null; return null;
} }
}; };
export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => { export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => {
try { try {
const res = await graphqlClient.query(DASHBOARD_CONFIG, {}); const res = await graphqlClient.query(DASHBOARD_CONFIG, {});
@ -55,9 +136,63 @@ export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | nu
console.error("Error fetching dashboard config:", res.error); console.error("Error fetching dashboard config:", res.error);
} }
return res.data?.dashboard_config || null; return (res.data?.dashboard_config as unknown as DashboardConfigType[]) || null;
} catch (error) { } catch (error) {
console.error("Error fetching dashboard config:", error); console.error("Error fetching dashboard config:", error);
return null; return null;
} }
}; };
export const fetchMentorCompetenceSummary = async (
courseId: string
): Promise<AssignmentsStatisticsType | null> => {
try {
const res = await graphqlClient.query(DASHBOARD_MENTOR_COMPETENCE_SUMMARY, {
courseId,
});
if (res.error) {
console.error("Error fetching data for course ID:", courseId, res.error);
}
return res.data?.mentor_course_statistics?.assignments || null;
} catch (error) {
console.error(`Error fetching data for course ID: ${courseId}`, error);
return null;
}
};
export async function fetchDashboardPersons(mode: DashboardPersonsPageMode) {
let url = "/api/dashboard/persons/";
if (mode === "competenceMetrics") {
url += "?with_competence_metrics=true";
}
return await itGetCached<DashboardPersonType[]>(url);
}
export async function fetchDashboardDueDates() {
return await itGetCached<DashboardDueDate[]>("/api/dashboard/duedates/");
}
export async function fetchDashboardConfigv2() {
return await itGetCached<DashboardCourseConfigType[]>("/api/dashboard/config/");
}
export async function fetchMenteeCount(courseId: string) {
return await itGetCached<{ mentee_count: number }>(
`/api/dashboard/course/${courseId}/mentees/`
);
}
export async function fetchOpenTasksCount(courseId: string) {
return await itGetCached<{ open_task_count: number }>(
`/api/dashboard/course/${courseId}/open_tasks/`
);
}
export function courseIdForCourseSlug(
dashboardConfigs: DashboardCourseConfigType[],
courseSlug: string
) {
const config = dashboardConfigs.find((config) => config.course_slug === courseSlug);
return config?.course_id;
}

View File

@ -16,7 +16,7 @@ export function useEntities() {
const countries: Ref<Country[]> = ref([]); const countries: Ref<Country[]> = ref([]);
const organisations: Ref<Organisation[]> = ref([]); const organisations: Ref<Organisation[]> = ref([]);
itGetCached("/api/core/entities/").then((res) => { itGetCached("/api/core/entities/").then((res: any) => {
countries.value = res.countries; countries.value = res.countries;
organisations.value = res.organisations; organisations.value = res.organisations;
}); });

View File

@ -84,7 +84,7 @@ export async function uploadCircleDocument(
throw new Error("No file selected"); throw new Error("No file selected");
} }
const startData = await startFileUpload(data, courseSessionId); const startData: any = await startFileUpload(data, courseSessionId);
await uploadFile(startData, data.file); await uploadFile(startData, data.file);
const response = itPost(`/api/core/file/finish/`, { const response = itPost(`/api/core/file/finish/`, {

View File

@ -69,7 +69,7 @@ export const useLearningMentees = (
error.value = null; error.value = null;
itGet(`/api/mentor/${courseSessionId}/summary`) itGet(`/api/mentor/${courseSessionId}/summary`)
.then((response) => { .then((response: any) => {
summary.value = response; summary.value = response;
}) })
.catch((err) => (error.value = err)) .catch((err) => (error.value = err))

View File

@ -46,7 +46,7 @@ export const useCompletionStore = defineStore({
} }
if (courseSessionId) { if (courseSessionId) {
const completionData = await itPost("/api/course/completion/mark/", { const completionData: any = await itPost("/api/course/completion/mark/", {
page_id: page.id, page_id: page.id,
completion_status: page.completion_status, completion_status: page.completion_status,
course_session_id: courseSessionId, course_session_id: courseSessionId,

View File

@ -1,9 +1,8 @@
import { itGetCached } from "@/fetchHelpers"; import { itGetCached } from "@/fetchHelpers";
import type { CourseSession, DueDate } from "@/types"; import type { CourseSession } from "@/types";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route"; import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -25,13 +24,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
const userStore = useUserStore(); const userStore = useUserStore();
if (userStore.loggedIn) { if (userStore.loggedIn) {
// TODO: refactor after implementing of Klassenkonzept
await Promise.all(
allCourseSessions.value.map(async (cs) => {
sortDueDates(cs.due_dates);
})
);
if (!allCourseSessions.value) { if (!allCourseSessions.value) {
throw `No courseSessionData found for user`; throw `No courseSessionData found for user`;
} }
@ -137,37 +129,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(hasPreview && (inLearningPath() || inCompetenceProfile())); return Boolean(hasPreview && (inLearningPath() || inCompetenceProfile()));
}); });
function allDueDates() {
const allDueDatesReturn: DueDate[] = [];
allCourseSessions.value?.forEach((cs) => {
allDueDatesReturn.push(...cs.due_dates);
});
sortDueDates(allDueDatesReturn);
return allDueDatesReturn;
}
function sortDueDates(dueDates: DueDate[]) {
dueDates.sort((a, b) => {
const dateA = dayjs(a.start);
const dateB = dayjs(b.start);
if (!dateA.isValid() && !dateB.isValid()) return 0; // If both are invalid, they are equal
if (!dateA.isValid()) return 1; // If dateA is invalid, it goes after dateB
if (!dateB.isValid()) return -1; // If dateB is invalid, it goes after dateA
return dateA.diff(dateB); // sort by `start`
});
}
return { return {
uniqueCourseSessionsByCourse, uniqueCourseSessionsByCourse,
allCurrentCourseSessions, allCurrentCourseSessions,
getCourseSessionById, getCourseSessionById,
switchCourseSessionById, switchCourseSessionById,
isCourseSessionPreviewActive, isCourseSessionPreviewActive,
allDueDates,
// use `useCurrentCourseSession` whenever possible // use `useCurrentCourseSession` whenever possible
currentCourseSession, currentCourseSession,

View File

@ -2,11 +2,11 @@ import type {
CourseProgressType, CourseProgressType,
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
DashboardType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import type { DashboardCourseConfigType } from "@/services/dashboard";
import { import {
fetchDashboardConfig, fetchDashboardConfig,
fetchProgressData, fetchDashboardConfigv2,
fetchStatisticData, fetchStatisticData,
} from "@/services/dashboard"; } from "@/services/dashboard";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -15,6 +15,7 @@ import { ref } from "vue";
export const useDashboardStore = defineStore("dashboard", () => { export const useDashboardStore = defineStore("dashboard", () => {
const dashboardConfigs: Ref<DashboardConfigType[]> = ref([]); const dashboardConfigs: Ref<DashboardConfigType[]> = ref([]);
const dashboardConfigsv2: Ref<DashboardCourseConfigType[]> = ref([]);
const currentDashboardConfig: Ref<DashboardConfigType | undefined> = ref(); const currentDashboardConfig: Ref<DashboardConfigType | undefined> = ref();
const dashBoardDataCache: Record< const dashBoardDataCache: Record<
string, string,
@ -24,21 +25,21 @@ export const useDashboardStore = defineStore("dashboard", () => {
ref(null); ref(null);
const loading = ref(false); const loading = ref(false);
const loadDashboardData = async (type: DashboardType, id: string) => { // const loadDashboardData = async (type: DashboardType, id: string) => {
let data; // let data;
switch (type) { // switch (type) {
case "STATISTICS_DASHBOARD": // case "STATISTICS_DASHBOARD":
data = await fetchStatisticData(id); // data = await fetchStatisticData(id);
break; // break;
case "PROGRESS_DASHBOARD": // case "PROGRESS_DASHBOARD":
data = await fetchProgressData(id); // data = await fetchProgressData(id);
break; // break;
default: // default:
return; // return;
} // }
dashBoardDataCache[id] = data; // dashBoardDataCache[id] = data;
currentDashBoardData.value = data; // currentDashBoardData.value = data;
}; // };
const switchAndLoadDashboardConfig = async (config: DashboardConfigType) => { const switchAndLoadDashboardConfig = async (config: DashboardConfigType) => {
currentDashboardConfig.value = config; currentDashboardConfig.value = config;
@ -56,29 +57,47 @@ export const useDashboardStore = defineStore("dashboard", () => {
const loadDashboardDetails = async () => { const loadDashboardDetails = async () => {
loading.value = true; loading.value = true;
dashboardConfigsv2.value = await fetchDashboardConfigv2();
console.log("got dashboard config v2: ", dashboardConfigsv2.value);
try { try {
if (!currentDashboardConfig.value) { // if (!currentDashboardConfig.value) {
await loadDashboardConfig(); // await loadDashboardConfig();
return; // return;
} // }
const { id, dashboard_type } = currentDashboardConfig.value; // const { id, dashboard_type } = currentDashboardConfig.value;
if (dashBoardDataCache[id]) { // if (dashBoardDataCache[id]) {
currentDashBoardData.value = dashBoardDataCache[id]; // currentDashBoardData.value = dashBoardDataCache[id];
return; // return;
} // }
await loadDashboardData(dashboard_type, id); // // await loadDashboardData(dashboard_type, id);
} finally { } finally {
console.log("done loading dashboard details");
loading.value = false; loading.value = false;
} }
}; };
const loadStatisticsData = async (id: string) => {
const data = await fetchStatisticData(id);
dashBoardDataCache[id] = data;
currentDashBoardData.value = data;
};
const loadStatisticsDatav2 = async (id: string) => {
const data = await fetchStatisticData(id);
dashBoardDataCache[id] = data;
return data;
};
return { return {
dashboardConfigs, dashboardConfigs,
dashboardConfigsv2,
currentDashboardConfig, currentDashboardConfig,
switchAndLoadDashboardConfig, switchAndLoadDashboardConfig,
loadDashboardConfig, loadDashboardConfig,
loadDashboardDetails, loadDashboardDetails,
currentDashBoardData, currentDashBoardData,
loading, loading,
loadStatisticsData,
loadStatisticsDatav2,
}; };
}); });

View File

@ -21,7 +21,7 @@ export const useMediaLibraryStore = defineStore({
return this.mediaLibraryPage; return this.mediaLibraryPage;
} }
log.debug("load mediaLibraryPageData"); log.debug("load mediaLibraryPageData");
const mediaLibraryPageData = await itGet(`/api/course/page/${slug}/`); const mediaLibraryPageData: any = await itGet(`/api/course/page/${slug}/`);
if (!mediaLibraryPageData) { if (!mediaLibraryPageData) {
throw `No mediaLibraryPageData found with: ${slug}`; throw `No mediaLibraryPageData found with: ${slug}`;

View File

@ -27,7 +27,7 @@ export const useNotificationsStore = defineStore("notifications", () => {
} }
async function updateUnreadCount() { async function updateUnreadCount() {
const data = await itGet("/notifications/api/unread_count/"); const data: any = await itGet("/notifications/api/unread_count/");
hasUnread.value = data.unread_count !== 0; hasUnread.value = data.unread_count !== 0;
} }

View File

@ -155,7 +155,7 @@ export const useUserStore = defineStore({
}); });
}, },
async fetchUser() { async fetchUser() {
const data = await itGetCached("/api/core/me/"); const data: any = await itGetCached("/api/core/me/");
this.$state = data; this.$state = data;
this.loggedIn = true; this.loggedIn = true;
await setLocale(data.language); await setLocale(data.language);

View File

@ -193,6 +193,8 @@ export interface CourseConfiguration {
enable_circle_documents: boolean; enable_circle_documents: boolean;
enable_learning_mentor: boolean; enable_learning_mentor: boolean;
enable_competence_certificates: boolean; enable_competence_certificates: boolean;
is_uk: boolean;
is_vv: boolean;
} }
export interface Course { export interface Course {
@ -207,6 +209,8 @@ export interface CourseCategory {
id: string; id: string;
name: string; name: string;
general: boolean; general: boolean;
is_uk: boolean;
is_vv: boolean;
} }
export type MediaLibraryContentBlockValue = { export type MediaLibraryContentBlockValue = {
@ -451,7 +455,6 @@ export interface CourseSession {
title: string; title: string;
start_date: string; start_date: string;
end_date: string; end_date: string;
due_dates: DueDate[];
actions: string[]; actions: string[];
} }
@ -607,3 +610,5 @@ export type User = {
course_session_experts: any[]; course_session_experts: any[];
language: string; language: string;
}; };
export type DashboardPersonsPageMode = "default" | "competenceMetrics";

View File

@ -1,41 +0,0 @@
import { login } from "./helpers";
// constants
const COURSE_SELECT = "[data-cy=appointments-course-select]";
const SESSION_SELECT = "[data-cy=appointments-session-select]";
const CIRCLE_SELECT = "[data-cy=appointments-circle-select]";
const APPOINTMENTS = "[data-cy=appointments-list]";
describe("appointments.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("test-student2@example.com", "test");
cy.visit("/course/test-lehrgang/appointments");
});
it("preselects first course (Test Lehrgang)", () => {
cy.visit("/course/test-lehrgang/appointments");
cy.get(COURSE_SELECT).should("contain", "Test Lehrgang");
cy.get(SESSION_SELECT).should("contain", "Bern");
cy.get(CIRCLE_SELECT).should("contain", "Alle");
cy.get(".cy-single-due-date").should("have.length", 5);
});
it("can filter by circle", () => {
cy.get(CIRCLE_SELECT).click();
cy.get(CIRCLE_SELECT).contains("Fahrzeug").click();
// THEN
cy.get(APPOINTMENTS).should("not.contain", "Keine Termine");
});
it("can switch course session", () => {
cy.get(SESSION_SELECT).click();
cy.get(SESSION_SELECT).contains("Zürich").click();
cy.get(SESSION_SELECT).should("contain", "Zürich");
// THEN
cy.get(APPOINTMENTS).should("contain", "Keine Termine");
});
});

View File

@ -101,7 +101,7 @@ describe("circle.cy.js", () => {
.should("contain", "Feedback"); .should("contain", "Feedback");
cy.visit("/course/test-lehrgang/learn/reisen"); cy.visit("/course/test-lehrgang/learn/reisen");
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3); cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 4);
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9); cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 11);
}); });
}); });

View File

@ -29,9 +29,9 @@ describe("selfEvaluation.cy.js", () => {
cy.get("[data-cy=\"self-evaluation-unknown\"]").should("have.text", "4"); cy.get("[data-cy=\"self-evaluation-unknown\"]").should("have.text", "4");
// learning unit id = 687 also known as: // learning unit id = 692 also known as:
// Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>> // Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>>
const identifier = "self-eval-687" const identifier = "self-eval-692"
// data in KompetenzNavi/Selbsteinschätzungen is correct // data in KompetenzNavi/Selbsteinschätzungen is correct
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback"); cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback");

View File

@ -53,7 +53,7 @@ describe("dashboardSupervisor.cy.js", () => {
}); });
it("contains correct details link", () => { it("contains correct details link", () => {
clickOnDetailsLink("attendance"); clickOnDetailsLink("attendance");
cy.url().should("contain", "/statistic/attendance"); cy.url().should("contain", "/statistic/test-lehrgang/attendance");
// might be improved: roughly check // might be improved: roughly check
// that the correct data is displayed // that the correct data is displayed
@ -63,14 +63,6 @@ describe("dashboardSupervisor.cy.js", () => {
}); });
}); });
describe("overall summary box", () => {
it("contains correct numbers (members, experts etc.)", () => {
getDashboardStatistics("participant.count").should("have.text", "4");
getDashboardStatistics("expert.count").should("have.text", "2");
getDashboardStatistics("session.count").should("have.text", "2");
});
});
describe("feedback summary box", () => { describe("feedback summary box", () => {
it("contains correct numbers", () => { it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3"); getDashboardStatistics("feedback.average").should("have.text", "3.3");
@ -78,7 +70,7 @@ describe("dashboardSupervisor.cy.js", () => {
}); });
it("contains correct details link", () => { it("contains correct details link", () => {
clickOnDetailsLink("feedback"); clickOnDetailsLink("feedback");
cy.url().should("contain", "/statistic/feedback"); cy.url().should("contain", "/statistic/test-lehrgang/feedback");
// might be improved: roughly check // might be improved: roughly check
// that the correct data is displayed // that the correct data is displayed
@ -96,7 +88,7 @@ describe("dashboardSupervisor.cy.js", () => {
}); });
it("contains correct details link", () => { it("contains correct details link", () => {
clickOnDetailsLink("competence"); clickOnDetailsLink("competence");
cy.url().should("contain", "/statistic/competence"); cy.url().should("contain", "/statistic/test-lehrgang/competence");
// might be improved: roughly check // might be improved: roughly check
// that the correct data is displayed // that the correct data is displayed

View File

@ -0,0 +1,49 @@
import { login } from "./helpers";
function selectDropboxItem(dropboxSelector, item) {
cy.get(dropboxSelector).click();
cy.get(dropboxSelector).contains(item).click();
}
describe("dueDates.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("test-student2@example.com", "test");
cy.visit("/dashboard/due-dates");
});
it("can filter due dates by dropbox selects", () => {
cy.get('[data-cy="due-date-list"]').children().should("have.length", 7);
// can filter by session
selectDropboxItem('[data-cy="select-session"]', "Zürich");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 1);
selectDropboxItem('[data-cy="select-session"]', "Bern");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 6);
selectDropboxItem('[data-cy="select-session"]', "Alle");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 7);
// can filter by circle
selectDropboxItem('[data-cy="select-circle"]', "Fahrzeug");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 6);
selectDropboxItem('[data-cy="select-circle"]', "Reisen");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 1);
selectDropboxItem('[data-cy="select-circle"]', "Alle");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 7);
// can filter by types
selectDropboxItem('[data-cy="select-type"]', "Präsenzkurs");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 3);
selectDropboxItem('[data-cy="select-type"]', "Bewertung");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 1);
// combination
selectDropboxItem('[data-cy="select-session"]', "Bern");
selectDropboxItem('[data-cy="select-type"]', "Präsenzkurs");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 2);
selectDropboxItem('[data-cy="select-session"]', "Zürich");
cy.get('[data-cy="due-date-list"]').children().should("have.length", 1);
selectDropboxItem('[data-cy="select-type"]', "Bewertung");
cy.get('[data-cy="due-date-list"]').should("contain", "Keine Termine");
});
});

View File

@ -1,11 +1,20 @@
export const MENTOR_OVERVIEW_URL = "/course/versicherungsvermittler-in/learning-mentor"; export const MENTOR_TASKS_URL_VV =
export const MENTOR_MENTEES_URL = "/course/versicherungsvermittler-in/learning-mentor/participants"; "/course/versicherungsvermittler-in/learning-mentor/tasks";
export const MENTOR_MENTEES_URL_VV =
"/course/versicherungsvermittler-in/learning-mentor";
export const MENTOR_MENTEES_URL_UK = "/course/test-lehrgang/learning-mentor";
export const MAIN_NAVIGATION_MENTOR_LINK =
"[data-cy=navigation-learning-mentor-link]";
export const MENTOR_DASHBOARD_LINK = "[data-cy=lm-dashboard-link]"; export const MENTOR_DASHBOARD_LINK = "[data-cy=lm-dashboard-link]";
export const MEMBER_DASHBOARD_LINK = "[data-cy=progress-dashboard-continue-course-link]"; export const MEMBER_DASHBOARD_LINK =
"[data-cy=progress-dashboard-continue-course-link]";
export const MENTOR_MAIN_NAVIGATION = "[data-cy=lm-main-navigation]"; export const MENTOR_MAIN_NAVIGATION = "[data-cy=lm-main-navigation]";
export const MENTOR_OVERVIEW_NAVIGATION_LINK = "[data-cy=lm-overview-navigation-link]"; export const MENTOR_OVERVIEW_NAVIGATION_LINK =
export const MENTOR_MENTEES_NAVIGATION_LINK = "[data-cy=lm-mentees-navigation-link]"; "[data-cy=lm-overview-navigation-link]";
export const MENTOR_MENTEES_NAVIGATION_LINK =
"[data-cy=lm-mentees-navigation-link]";
// /participants // /participants
export const MENTOR_MY_MENTEES = "[data-cy=lm-my-mentees]"; export const MENTOR_MY_MENTEES = "[data-cy=lm-my-mentees]";
@ -17,7 +26,5 @@ export const MENTOR_MENTEE_PROFILE = "[data-cy=lm-my-mentee-profile]";
export const MENTEE_MENTOR_LIST_ITEM = "[data-cy=lm-my-mentor-list-item]"; export const MENTEE_MENTOR_LIST_ITEM = "[data-cy=lm-my-mentor-list-item]";
export const MENTEE_MENTOR_REMOVE = "[data-cy=lm-my-mentor-remove]"; export const MENTEE_MENTOR_REMOVE = "[data-cy=lm-my-mentor-remove]";
export const MENTEE_MENTORS_TITLE = "[data-cy=lm-my-lms-title]";
export const MENTEE_INVITE_MENTOR = "[data-cy=lm-invite-mentor-button]";

View File

@ -1,14 +1,18 @@
import {login} from "../../helpers"; import { login } from "../../helpers";
import { import {
MAIN_NAVIGATION_MENTOR_LINK,
MEMBER_DASHBOARD_LINK, MEMBER_DASHBOARD_LINK,
MENTEE_INVITE_MENTOR,
MENTEE_MENTOR_LIST_ITEM, MENTEE_MENTOR_LIST_ITEM,
MENTEE_MENTOR_REMOVE, MENTEE_MENTOR_REMOVE,
MENTEE_MENTORS_TITLE,
MENTOR_MENTEES_NAVIGATION_LINK, MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL, MENTOR_MENTEES_URL_UK,
MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES, MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS, MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK, MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL MENTOR_TASKS_URL_VV,
} from "../constants"; } from "../constants";
describe("memberOnly.cy.js", () => { describe("memberOnly.cy.js", () => {
@ -23,22 +27,22 @@ describe("memberOnly.cy.js", () => {
}); });
it("shows NO mentees navigation link", () => { it("shows NO mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).should("not.exist"); cy.get(MENTOR_MENTEES_NAVIGATION_LINK).should("not.exist");
}) });
it("shows NO overview navigation link", () => { it("shows NO overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).should("not.exist"); cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).should("not.exist");
}) });
it("shows NO mentees", () => { it("shows NO mentees", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("not.exist"); cy.get(MENTOR_MY_MENTEES).should("not.exist");
}); });
it("shows my mentors", () => { it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("exist"); cy.get(MENTOR_MY_MENTORS).should("exist");
}); });
@ -47,12 +51,44 @@ describe("memberOnly.cy.js", () => {
const mentor = "Micheala Weber-Mentor"; const mentor = "Micheala Weber-Mentor";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor) cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE) .find(MENTEE_MENTOR_REMOVE)
.click(); .click();
// then // then
cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist"); cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist");
}) });
it("uses term Lernbegleitung in VV-course", () => {
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MAIN_NAVIGATION_MENTOR_LINK).should("contain", "Lernbegleitung");
cy.get(MENTEE_MENTORS_TITLE).should("contain", "Meine Lernbegleiter");
cy.get(MENTEE_INVITE_MENTOR).should(
"contain",
"Neue Lernbegleitung einladen"
);
});
});
describe("memberOnly.cy.js üK", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --set-only-is-uk-flag");
login("test-student1@example.com", "test");
});
it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL_UK);
cy.get(MENTOR_MY_MENTORS).should("exist");
});
it("uses term Praxisbildner in UK-course", () => {
cy.visit(MENTOR_MENTEES_URL_UK);
cy.get(MAIN_NAVIGATION_MENTOR_LINK).should("contain", "Praxisbildner");
cy.get(MENTEE_MENTORS_TITLE).should("contain", "Meine Praxisbildner");
cy.get(MENTEE_INVITE_MENTOR).should(
"contain",
"Neuen Praxisbildner einladen"
);
});
}); });

View File

@ -1,4 +1,4 @@
import {login} from "../../helpers"; import { login } from "../../helpers";
import { import {
MEMBER_DASHBOARD_LINK, MEMBER_DASHBOARD_LINK,
MENTEE_MENTOR_LIST_ITEM, MENTEE_MENTOR_LIST_ITEM,
@ -8,11 +8,11 @@ import {
MENTOR_MENTEE_PROFILE, MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE, MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK, MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL, MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES, MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS, MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK, MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL MENTOR_TASKS_URL_VV,
} from "../constants"; } from "../constants";
describe("mentorAndMember.cy.js", () => { describe("mentorAndMember.cy.js", () => {
@ -27,34 +27,34 @@ describe("mentorAndMember.cy.js", () => {
}); });
it("shows the learning mentor navigation", () => { it("shows the learning mentor navigation", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist"); cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
}); });
it("shows the mentees navigation link", () => { it("shows the mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click(); cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_MENTEES_URL); cy.url().should("include", MENTOR_MENTEES_URL_VV);
}) });
it("shows the overview navigation link", () => { it("shows the overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click(); cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL); cy.url().should("include", MENTOR_TASKS_URL_VV);
}) });
it("shows my mentees", () => { it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("exist"); cy.get(MENTOR_MY_MENTEES).should("exist");
}); });
it("shows my mentors", () => { it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("exist"); cy.get(MENTOR_MY_MENTORS).should("exist");
}); });
it("shows the correct mentees", () => { it("shows the correct mentees", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas"); cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas");
}); });
@ -63,50 +63,55 @@ describe("mentorAndMember.cy.js", () => {
const mentee = "Viktor Vollgas"; const mentee = "Viktor Vollgas";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee) cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE) .find(MENTOR_MENTEE_PROFILE)
.click(); .click();
// then // then
const expectedMenteeProfileUrl = "/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a" const expectedMenteeProfileUrl =
"/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a";
cy.url().should("include", expectedMenteeProfileUrl); cy.url().should("include", expectedMenteeProfileUrl);
cy.contains(mentee).should("exist"); cy.contains(mentee).should("exist");
}) });
it("can remove a mentee", () => { it("can remove a mentee", () => {
// given // given
const mentee = "Viktor Vollgas"; const mentee = "Viktor Vollgas";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee) cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE) .find(MENTOR_MENTEE_REMOVE)
.click(); .click();
// then // then
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist"); cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist");
cy.contains("Aktuell begleitest du niemanden als Lernbegleitung").should("exist"); cy.contains("Aktuell begleitest du niemanden als Lernbegleitung").should(
}) "exist"
);
});
it("shows the correct mentors", () => { it("shows the correct mentors", () => {
const mentor = "Micheala Weber-Mentor"; const mentor = "Micheala Weber-Mentor";
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("contain", mentor); cy.get(MENTOR_MY_MENTORS).should("contain", mentor);
}) });
it("can remove a mentor", () => { it("can remove a mentor", () => {
// given // given
const mentor = "Micheala Weber-Mentor"; const mentor = "Micheala Weber-Mentor";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor) cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE) .find(MENTEE_MENTOR_REMOVE)
.click(); .click();
// then // then
cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist"); cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist");
cy.contains("Aktuell hast du noch keine Person als Lernbegleitung eingeladen").should("exist"); cy.contains(
}) "Aktuell hast du noch keine Person als Lernbegleitung eingeladen"
).should("exist");
});
}); });

View File

@ -1,4 +1,4 @@
import {login} from "../../helpers"; import { login } from "../../helpers";
import { import {
MENTOR_DASHBOARD_LINK, MENTOR_DASHBOARD_LINK,
MENTOR_MAIN_NAVIGATION, MENTOR_MAIN_NAVIGATION,
@ -6,11 +6,11 @@ import {
MENTOR_MENTEE_PROFILE, MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE, MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK, MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL, MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES, MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS, MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK, MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL MENTOR_TASKS_URL_VV,
} from "../constants"; } from "../constants";
describe("mentorOnly.cy.js", () => { describe("mentorOnly.cy.js", () => {
@ -22,38 +22,38 @@ describe("mentorOnly.cy.js", () => {
it("shows the correct dashboard", () => { it("shows the correct dashboard", () => {
cy.visit("/"); cy.visit("/");
cy.get(MENTOR_DASHBOARD_LINK).click(); cy.get(MENTOR_DASHBOARD_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL); cy.url().should("include", MENTOR_MENTEES_URL_VV);
}); });
it("shows the learning mentor navigation", () => { it("shows the learning mentor navigation", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist"); cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
}); });
it("shows the mentees navigation link", () => { it("shows the mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click(); cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_MENTEES_URL); cy.url().should("include", MENTOR_MENTEES_URL_VV);
}) });
it("shows the overview navigation link", () => { it("shows the overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL); cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click(); cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL); cy.url().should("include", MENTOR_TASKS_URL_VV);
}) });
it("shows my mentees", () => { it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("exist"); cy.get(MENTOR_MY_MENTEES).should("exist");
}); });
it("shows no mentors", () => { it("shows no mentors", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("not.exist"); cy.get(MENTOR_MY_MENTORS).should("not.exist");
}); });
it("shows the correct mentees", () => { it("shows the correct mentees", () => {
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("contain", "Robert Student-plus-Mentor"); cy.get(MENTOR_MY_MENTEES).should("contain", "Robert Student-plus-Mentor");
cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas"); cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas");
}); });
@ -63,29 +63,32 @@ describe("mentorOnly.cy.js", () => {
const mentee = "Viktor Vollgas"; const mentee = "Viktor Vollgas";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee) cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE) .find(MENTOR_MENTEE_PROFILE)
.click(); .click();
// then // then
const expectedMenteeProfileUrl = "/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a" const expectedMenteeProfileUrl =
"/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a";
cy.url().should("include", expectedMenteeProfileUrl); cy.url().should("include", expectedMenteeProfileUrl);
cy.contains(mentee).should("exist"); cy.contains(mentee).should("exist");
}) });
it("can remove a mentee", () => { it("can remove a mentee", () => {
// given // given
const mentee = "Viktor Vollgas"; const mentee = "Viktor Vollgas";
// when // when
cy.visit(MENTOR_MENTEES_URL); cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee) cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE) .find(MENTOR_MENTEE_REMOVE)
.click(); .click();
// then // then
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist"); cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist");
cy.contains(MENTOR_MENTEE_LIST_ITEM, "Robert Student-plus-Mentor").should("exist") cy.contains(MENTOR_MENTEE_LIST_ITEM, "Robert Student-plus-Mentor").should(
}) "exist"
);
});
}); });

View File

@ -16,17 +16,19 @@ describe("login.cy.js", () => {
cy.get("#username").type("test-student1@example.com"); cy.get("#username").type("test-student1@example.com");
cy.get("#password").type("test"); cy.get("#password").type("test");
cy.get("[data-cy=\"login-button\"]").click(); cy.get('[data-cy="login-button"]').click();
cy.request("/api/core/me").its("status").should("eq", 200); cy.request("/api/core/me").its("status").should("eq", 200);
cy.get("[data-cy=\"dashboard-title\"]").should("contain", "Dashboard"); cy.get('[data-cy="db-course-title"]')
.first()
.should("contain", "Test Lehrgang");
}); });
it("can login with helper function", () => { it("can login with helper function", () => {
login("test-student1@example.com", "test"); login("test-student1@example.com", "test");
cy.visit("/"); cy.visit("/");
cy.request("/api/core/me").its("status").should("eq", 200); cy.request("/api/core/me").its("status").should("eq", 200);
cy.get("[data-cy=\"dashboard-title\"]").should("contain", "Dashboard"); cy.get('[data-cy="db-course-title"]').should("contain", "Test Lehrgang");
}); });
it("login will redirect to requested page", () => { it("login will redirect to requested page", () => {
@ -36,9 +38,9 @@ describe("login.cy.js", () => {
cy.get("#username").type("test-student1@example.com"); cy.get("#username").type("test-student1@example.com");
cy.get("#password").type("test"); cy.get("#password").type("test");
cy.get("[data-cy=\"login-button\"]").click(); cy.get('[data-cy="login-button"]').click();
cy.get("[data-cy=\"learning-path-title\"]").should( cy.get('[data-cy="learning-path-title"]').should(
"contain", "contain",
"Test Lehrgang" "Test Lehrgang"
); );

View File

@ -0,0 +1,12 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
from environs import Env
script_path = os.path.abspath(__file__)
script_dir = os.path.dirname(script_path)
env = Env()
env.read_env(f"{script_dir}/../../../env_secrets/local_daniel.env", recurse=False)
from .base import * # noqa

View File

@ -39,6 +39,13 @@ from vbv_lernwelt.course.views import (
request_course_completion_for_user, request_course_completion_for_user,
) )
from vbv_lernwelt.course_session.views import get_course_session_documents from vbv_lernwelt.course_session.views import get_course_session_documents
from vbv_lernwelt.dashboard.views import (
get_dashboard_config,
get_dashboard_due_dates,
get_dashboard_persons,
get_mentee_count,
get_mentor_open_tasks_count,
)
from vbv_lernwelt.edoniq_test.views import ( from vbv_lernwelt.edoniq_test.views import (
export_students, export_students,
export_students_and_trainers, export_students_and_trainers,
@ -116,6 +123,14 @@ urlpatterns = [
re_path(r"api/notify/email_notification_settings/$", email_notification_settings, re_path(r"api/notify/email_notification_settings/$", email_notification_settings,
name='email_notification_settings'), name='email_notification_settings'),
# dashboard
path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"),
path(r"api/dashboard/duedates/", get_dashboard_due_dates, name="get_dashboard_due_dates"),
path(r"api/dashboard/config/", get_dashboard_config, name="get_dashboard_config"),
path(r"api/dashboard/course/<str:course_id>/mentees/", get_mentee_count, name="get_mentee_count"),
path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count,
name="get_mentor_open_tasks_count"),
# course # course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view, path(r"api/course/page/<slug_or_id>/", course_page_api_view,

View File

@ -26,7 +26,9 @@ from wagtail.blocks.list_block import ListBlock, ListValue
from wagtail.rich_text import RichText from wagtail.rich_text import RichText
def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None): def create_uk_fahrzeug_casework(
course_id=COURSE_UK, competence_certificate=None, with_documents=False
):
assignment_list_page = ( assignment_list_page = (
CoursePage.objects.get(course_id=course_id) CoursePage.objects.get(course_id=course_id)
.get_children() .get_children()
@ -40,7 +42,6 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True, needs_expert_evaluation=True,
competence_certificate=competence_certificate, competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden", effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
<h3>Ausgangslage</h3> <h3>Ausgangslage</h3>
@ -70,6 +71,11 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf", evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.", evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
) )
if with_documents:
assignment.solution_sample = ContentDocument.objects.get(
title="Musterlösung Fahrzeug"
)
assignment.save()
assignment.evaluation_tasks = [] assignment.evaluation_tasks = []
assignment.evaluation_tasks.append( assignment.evaluation_tasks.append(
@ -3591,7 +3597,7 @@ def create_uk_reflection(course_id=COURSE_UK):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="ca. 1 Stunde", effort_required="ca. 1 Stunde",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -3747,7 +3753,7 @@ def create_uk_fr_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="", effort_required="",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -3900,7 +3906,7 @@ def create_uk_it_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Riflessione", title="Riflessione",
effort_required="", effort_required="",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -4053,7 +4059,7 @@ def create_vv_reflection(
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="ca. 1 Stunde", effort_required="ca. 1 Stunde",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """

View File

@ -101,6 +101,10 @@ class AssignmentObjectType(DjangoObjectType):
lp = self.find_attached_learning_content() lp = self.find_attached_learning_content()
if lp: if lp:
learning_content_page_id = lp.id learning_content_page_id = lp.id
if not assignment_user_id:
assignment_user_id = getattr(info.context, "assignment_user_id", None)
return resolve_assignment_completion( return resolve_assignment_completion(
info=info, info=info,
course_session_id=course_session_id, course_session_id=course_session_id,

View File

@ -9,6 +9,8 @@ from vbv_lernwelt.competence.models import (
CompetenceCertificateList, CompetenceCertificateList,
) )
from vbv_lernwelt.course.graphql.types import resolve_course_page from vbv_lernwelt.course.graphql.types import resolve_course_page
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.iam.permissions import can_view_profile
class CompetenceCertificateQuery(object): class CompetenceCertificateQuery(object):
@ -24,6 +26,15 @@ class CompetenceCertificateQuery(object):
course_slug=graphene.String(), course_slug=graphene.String(),
) )
competence_certificate_list_for_user = graphene.Field(
CompetenceCertificateListObjectType,
id=graphene.ID(),
slug=graphene.String(),
course_id=graphene.ID(),
course_slug=graphene.String(),
user_id=graphene.UUID(),
)
def resolve_competence_certificate(root, info, id=None, slug=None): def resolve_competence_certificate(root, info, id=None, slug=None):
return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug) return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug)
@ -39,3 +50,26 @@ class CompetenceCertificateQuery(object):
course_id=course_id, course_id=course_id,
course_slug=course_slug, course_slug=course_slug,
) )
def resolve_competence_certificate_list_for_user(
root, info, id=None, slug=None, course_id=None, course_slug=None, user_id=None
):
try:
course_session_user = CourseSessionUser.objects.get(user__id=user_id)
except CourseSessionUser.DoesNotExist:
return None
if not can_view_profile(info.context.user, course_session_user):
return None
setattr(info.context, "assignment_user_id", user_id)
return resolve_course_page(
CompetenceCertificateList,
root,
info,
id=id,
slug=slug,
course_id=course_id,
course_slug=course_slug,
)

View File

@ -0,0 +1,42 @@
from vbv_lernwelt.assignment.models import AssignmentType
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionEdoniqTest,
)
def query_competence_course_session_assignments(course_session_ids, circle_ids=None):
if circle_ids is None:
circle_ids = []
result = []
for csa in CourseSessionAssignment.objects.filter(
course_session_id__in=course_session_ids,
learning_content__content_assignment__assignment_type__in=[
AssignmentType.CASEWORK.value,
],
learning_content__content_assignment__competence_certificate__isnull=False,
):
if circle_ids and csa.learning_content.get_circle().id not in circle_ids:
continue
result.append(csa)
return result
def query_competence_course_session_edoniq_tests(course_session_ids, circle_ids=None):
if circle_ids is None:
circle_ids = []
result = []
for cset in CourseSessionEdoniqTest.objects.filter(
course_session_id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False,
):
if circle_ids and cset.learning_content.get_circle().id not in circle_ids:
continue
result.append(cset)
return result

View File

@ -0,0 +1,307 @@
from django.utils import timezone
from graphene_django.utils import GraphQLTestCase
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.creators.test_utils import (
add_course_session_group_supervisor,
add_course_session_user,
create_course_session_group,
create_user,
)
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
class TestCertificateList(GraphQLTestCase):
GRAPHQL_URL = "/server/graphql/"
def setUp(self):
create_default_users()
self.course = create_test_course(include_vv=False, with_sessions=True)
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
assignment = Assignment.objects.get(
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
)
self.supervisor = create_user("supervisor")
group = create_course_session_group(course_session=self.course_session)
add_course_session_group_supervisor(group=group, user=self.supervisor)
self.member_one = create_user("member one")
add_course_session_user(
course_session=self.course_session,
user=self.member_one,
role=CourseSessionUser.Role.MEMBER,
)
self.member_two = create_user("member two")
add_course_session_user(
course_session=self.course_session,
user=self.member_two,
role=CourseSessionUser.Role.MEMBER,
)
AssignmentCompletion.objects.create(
assignment_user=self.member_one,
assignment=assignment,
learning_content_page=assignment.find_attached_learning_content(),
course_session=self.course_session,
completion_status="SUBMITTED",
submitted_at=timezone.now(),
completion_data={},
evaluation_max_points=10,
evaluation_points=5,
evaluation_passed=False,
)
AssignmentCompletion.objects.create(
assignment_user=self.member_two,
assignment=assignment,
learning_content_page=assignment.find_attached_learning_content(),
course_session=self.course_session,
completion_status="SUBMITTED",
submitted_at=timezone.now(),
completion_data={},
evaluation_max_points=10,
evaluation_points=10,
evaluation_passed=True,
)
def test_supervisor_userprofile_certificate_summary(self):
self.client.force_login(self.supervisor)
query = f"""query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
$userId: UUID!
) {{
competence_certificate_list_for_user(
course_slug: $courseSlug,
user_id: $userId
) {{
...CoursePageFields
competence_certificates {{
...CoursePageFields
assignments {{
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
...CoursePageFields
circle {{
id
title
slug
__typename
}}
__typename
}}
__typename
}}
__typename
}}
__typename
}}
}}
fragment CoursePageFields on CoursePageInterface {{
title
id
slug
content_type
frontend_url
__typename
}}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userId": str(self.member_one.id),
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
certificates = response.json()["data"]["competence_certificate_list_for_user"][
"competence_certificates"
]
self.assertEqual(len(certificates), 1)
assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completion"]
self.assertEqual(completion1["completion_status"], "SUBMITTED")
self.assertEqual(completion1["evaluation_points"], 5)
self.assertEqual(completion1["evaluation_max_points"], 10)
self.assertEqual(completion1["evaluation_passed"], False)
completion2 = assignments[1]["completion"]
self.assertIsNone(completion2)
def test_member_cannot_see_other_user_certificate_summary(self):
self.client.force_login(self.member_one)
query = f"""query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
$userId: UUID!
) {{
competence_certificate_list_for_user(
course_slug: $courseSlug,
user_id: $userId
) {{
...CoursePageFields
competence_certificates {{
...CoursePageFields
assignments {{
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
...CoursePageFields
circle {{
id
title
slug
__typename
}}
__typename
}}
__typename
}}
__typename
}}
__typename
}}
}}
fragment CoursePageFields on CoursePageInterface {{
title
id
slug
content_type
frontend_url
__typename
}}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userId": str(self.member_two.id),
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
self.assertIsNone(
response.json()["data"]["competence_certificate_list_for_user"]
)
def test_member_userprofile_certificate_summary(self):
self.client.force_login(self.member_one)
query = f"""query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
) {{
competence_certificate_list(
course_slug: $courseSlug,
) {{
...CoursePageFields
competence_certificates {{
...CoursePageFields
assignments {{
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
...CoursePageFields
circle {{
id
title
slug
__typename
}}
__typename
}}
__typename
}}
__typename
}}
__typename
}}
}}
fragment CoursePageFields on CoursePageInterface {{
title
id
slug
content_type
frontend_url
__typename
}}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
certificates = response.json()["data"]["competence_certificate_list"][
"competence_certificates"
]
self.assertEqual(len(certificates), 1)
assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completion"]
self.assertEqual(completion1["completion_status"], "SUBMITTED")
self.assertEqual(completion1["evaluation_points"], 5)
self.assertEqual(completion1["evaluation_max_points"], 10)
self.assertEqual(completion1["evaluation_passed"], False)
completion2 = assignments[1]["completion"]
self.assertIsNone(completion2)

View File

@ -98,6 +98,11 @@ from vbv_lernwelt.self_evaluation_feedback.models import (
default=False, default=False,
help="Will create a learning mentor for test user", help="Will create a learning mentor for test user",
) )
@click.option(
"--set-only-is-uk-flag/--no-set-only-is-uk-flag",
default=False,
help="Will set only the is_uk flag for the test course and enable learning mentors for the course",
)
def command( def command(
create_assignment_completion, create_assignment_completion,
create_assignment_evaluation, create_assignment_evaluation,
@ -108,6 +113,7 @@ def command(
create_attendance_days, create_attendance_days,
enable_circle_documents, enable_circle_documents,
create_learning_mentor, create_learning_mentor,
set_only_is_uk_flag,
): ):
print("cypress reset data") print("cypress reset data")
CourseCompletion.objects.all().delete() CourseCompletion.objects.all().delete()
@ -391,4 +397,13 @@ def command(
course = Course.objects.get(id=COURSE_TEST_ID) course = Course.objects.get(id=COURSE_TEST_ID)
course.configuration.enable_circle_documents = enable_circle_documents course.configuration.enable_circle_documents = enable_circle_documents
course.configuration.is_uk = True
if set_only_is_uk_flag:
course.configuration.is_vv = False
course.configuration.enable_learning_mentor = True
else:
course.configuration.is_vv = True
course.configuration.enable_learning_mentor = False
course.configuration.save() course.configuration.save()

View File

@ -1,10 +1,13 @@
import uuid import uuid
import structlog
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import JSONField from django.db.models import JSONField
from django.urls import reverse from django.urls import reverse
logger = structlog.get_logger(__name__)
class Organisation(models.Model): class Organisation(models.Model):
organisation_id = models.IntegerField(primary_key=True) organisation_id = models.IntegerField(primary_key=True)
@ -107,15 +110,24 @@ class User(AbstractUser):
blank=True, blank=True,
) )
def create_avatar_url(self, size=400):
try:
if self.avatar:
filter_spec = f"fill-{size}x{size}"
self.avatar.get_rendition(filter_spec)
url = reverse("user_image", kwargs={"image_id": self.avatar.id})
return f"{url}?filter={filter_spec}"
except Exception:
logger.warn("could not create avatar url", label="security", exc_info=True)
return "/static/avatars/myvbv-default-avatar.png"
@property @property
def avatar_url(self): def avatar_url(self):
if self.avatar: return self.create_avatar_url()
filter_spec = "fill-400x400"
self.avatar.get_rendition(filter_spec) @property
url = reverse("user_image", kwargs={"image_id": self.avatar.id}) def avatar_url_small(self):
return f"{url}?filter={filter_spec}" return self.create_avatar_url(size=96)
else:
return "/static/avatars/myvbv-default-avatar.png"
class SecurityRequestResponseLog(models.Model): class SecurityRequestResponseLog(models.Model):

View File

@ -23,6 +23,8 @@ class CourseConfigurationAdmin(admin.ModelAdmin):
"enable_circle_documents", "enable_circle_documents",
"enable_learning_mentor", "enable_learning_mentor",
"enable_competence_certificates", "enable_competence_certificates",
"is_vv",
"is_uk",
] ]

View File

@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from dateutil.relativedelta import MO, relativedelta, TH, TU from dateutil.relativedelta import MO, relativedelta, TH, TU, WE
from django.utils import timezone from django.utils import timezone
from slugify import slugify from slugify import slugify
from wagtail.rich_text import RichText from wagtail.rich_text import RichText
@ -47,6 +47,7 @@ from vbv_lernwelt.course.factories import CoursePageFactory
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
Course, Course,
CourseCategory, CourseCategory,
CourseConfiguration,
CoursePage, CoursePage,
CourseSession, CourseSession,
CourseSessionUser, CourseSessionUser,
@ -97,16 +98,18 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
) )
def create_test_course(include_uk=True, include_vv=True, with_sessions=False): def create_test_course(
include_uk=True, include_vv=True, with_sessions=False, with_documents=False
):
# create_locales_for_wagtail() # create_locales_for_wagtail()
create_default_collections() create_default_collections()
create_default_content_documents() if with_documents:
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0: create_default_content_documents()
create_default_images() if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
create_default_images()
course: Course = create_test_course_with_categories() course: Course = create_test_course_with_categories()
course.configuration.enable_learning_mentor = False course.configuration.enable_learning_mentor = False
course.configuration.save()
competence_certificate = create_test_competence_navi() competence_certificate = create_test_competence_navi()
@ -118,7 +121,9 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
if include_uk: if include_uk:
create_uk_fahrzeug_casework( create_uk_fahrzeug_casework(
course_id=COURSE_TEST_ID, competence_certificate=competence_certificate course_id=COURSE_TEST_ID,
competence_certificate=competence_certificate,
with_documents=with_documents,
) )
create_uk_fahrzeug_prep_assignment(course_id=COURSE_TEST_ID) create_uk_fahrzeug_prep_assignment(course_id=COURSE_TEST_ID)
create_uk_condition_acceptance(course_id=COURSE_TEST_ID) create_uk_condition_acceptance(course_id=COURSE_TEST_ID)
@ -128,11 +133,17 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
title="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)", title="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)",
competence_certificate=competence_certificate, competence_certificate=competence_certificate,
) )
course.configuration.is_uk = True
if include_vv: if include_vv:
create_vv_gewinnen_casework(course_id=COURSE_TEST_ID) create_vv_gewinnen_casework(course_id=COURSE_TEST_ID)
course.configuration.is_vv = True
create_test_learning_path(include_uk=include_uk, include_vv=include_vv) course.configuration.save()
create_test_learning_path(
include_uk=include_uk, include_vv=include_vv, with_documents=with_documents
)
create_test_media_library() create_test_media_library()
if with_sessions: if with_sessions:
@ -153,17 +164,41 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch", trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
) )
tuesday_in_one_week = ( wednesday_in_four_weeks = (
datetime.now() + relativedelta(weekday=TU) + relativedelta(weeks=1) datetime.now() + relativedelta(weekday=TU) + relativedelta(weeks=1)
) )
csac.due_date.start = timezone.make_aware( csac.due_date.start = timezone.make_aware(
tuesday_in_one_week.replace(hour=8, minute=30, second=0, microsecond=0) wednesday_in_four_weeks.replace(hour=8, minute=30, second=0, microsecond=0)
) )
csac.due_date.end = timezone.make_aware( csac.due_date.end = timezone.make_aware(
tuesday_in_one_week.replace(hour=17, minute=0, second=0, microsecond=0) wednesday_in_four_weeks.replace(hour=17, minute=0, second=0, microsecond=0)
) )
csac.due_date.save() csac.due_date.save()
if include_vv:
csac = CourseSessionAttendanceCourse.objects.create(
course_session=cs_bern,
learning_content=LearningContentAttendanceCourse.objects.get(
slug="test-lehrgang-lp-circle-reisen-lc-präsenzkurs-reisen"
),
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
)
wednesday_in_four_weeks = (
datetime.now() + relativedelta(weekday=TU) + relativedelta(weeks=4)
)
csac.due_date.start = timezone.make_aware(
wednesday_in_four_weeks.replace(
hour=8, minute=15, second=0, microsecond=0
)
)
csac.due_date.end = timezone.make_aware(
wednesday_in_four_weeks.replace(
hour=17, minute=30, second=0, microsecond=0
)
)
csac.due_date.save()
csa = CourseSessionAssignment.objects.create( csa = CourseSessionAssignment.objects.create(
course_session=cs_bern, course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get( learning_content=LearningContentAssignment.objects.get(
@ -187,7 +222,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
csa = CourseSessionAssignment.objects.create( csa = CourseSessionAssignment.objects.create(
course_session=cs_bern, course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get( learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto" slug="test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
), ),
) )
next_monday = datetime.now() + relativedelta(weekday=MO(2)) next_monday = datetime.now() + relativedelta(weekday=MO(2))
@ -215,7 +250,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
_csa = CourseSessionAssignment.objects.create( _csa = CourseSessionAssignment.objects.create(
course_session=cs_bern, course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get( learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm" slug="test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"
), ),
) )
@ -226,6 +261,24 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
id=TEST_COURSE_SESSION_ZURICH_ID, id=TEST_COURSE_SESSION_ZURICH_ID,
start_date=now, start_date=now,
) )
csac = CourseSessionAttendanceCourse.objects.create(
course_session=cs_zurich,
learning_content=LearningContentAttendanceCourse.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
),
location="siehe Anzeigetafel, Bildungszentrum Jungholz, Jungholzstrasse 43, 8050 Zürich-Oerlikon",
trainer="Catia Logarzo",
)
wednesday_in_four_weeks = (
datetime.now() + relativedelta(weekday=WE) + relativedelta(weeks=1)
)
csac.due_date.start = timezone.make_aware(
wednesday_in_four_weeks.replace(hour=8, minute=30, second=0, microsecond=0)
)
csac.due_date.end = timezone.make_aware(
wednesday_in_four_weeks.replace(hour=17, minute=0, second=0, microsecond=0)
)
csac.due_date.save()
region1 = CourseSessionGroup.objects.create( region1 = CourseSessionGroup.objects.create(
name="Region 1", name="Region 1",
@ -415,23 +468,29 @@ def create_test_course_with_categories(apps=None, schema_editor=None):
course.slug = course_page.slug course.slug = course_page.slug
course.save() course.save()
config = CourseConfiguration.objects.get_or_create(course=course)[0]
config.is_uk = True
config.save()
return course return course
def create_test_learning_path(include_uk=True, include_vv=True): def create_test_learning_path(include_uk=True, include_vv=True, with_documents=False):
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID) course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
lp = LearningPathFactory(title="Test Lernpfad", parent=course_page) lp = LearningPathFactory(title="Test Lernpfad", parent=course_page)
if include_uk: if include_uk:
TopicFactory(title="Circle ÜK", is_visible=False, parent=lp) TopicFactory(title="Circle ÜK", is_visible=False, parent=lp)
create_test_uk_circle_fahrzeug(lp, title="Fahrzeug") create_test_uk_circle_fahrzeug(
lp, title="Fahrzeug", with_documents=with_documents
)
if include_vv: if include_vv:
TopicFactory(title="Circle VV", is_visible=False, parent=lp) TopicFactory(title="Circle VV", is_visible=False, parent=lp)
create_test_circle_reisen(lp) create_test_circle_reisen(lp)
def create_test_uk_circle_fahrzeug(lp, title="Fahrzeug"): def create_test_uk_circle_fahrzeug(lp, title="Fahrzeug", with_documents=False):
circle = CircleFactory( circle = CircleFactory(
title=title, title=title,
parent=lp, parent=lp,
@ -470,21 +529,25 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
), ),
content_url=f"/course/{lp.get_course().slug}/media/handlungsfelder/{slugify(title)}", content_url=f"/course/{lp.get_course().slug}/media/handlungsfelder/{slugify(title)}",
) )
LearningContentAssignmentFactory( (
title="Redlichkeitserklärung", LearningContentAssignmentFactory(
parent=circle, title="Redlichkeitserklärung",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-redlichkeits" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-redlichkeits"
),
), ),
), )
LearningContentAssignmentFactory( (
title="Fahrzeug - Mein erstes Auto", LearningContentAssignmentFactory(
assignment_type="PREP_ASSIGNMENT", title="Fahrzeug - Mein erstes Auto",
parent=circle, assignment_type="PREP_ASSIGNMENT",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto"
),
), ),
), )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="X1"), parent=ActionCompetence.objects.get(competence_id="X1"),
@ -531,21 +594,24 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
LearningUnitFactory(title="Transfer", parent=circle) LearningUnitFactory(title="Transfer", parent=circle)
LearningContentAssignmentFactory( (
title="Reflexion", LearningContentAssignmentFactory(
assignment_type="REFLECTION", title="Reflexion",
parent=circle, assignment_type="REFLECTION",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith=f"test-lehrgang-assignment-reflexion" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-reflexion"
),
), ),
), )
assignment = Assignment.objects.get( assignment = Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs" slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
) )
assignment.solution_sample = ContentDocument.objects.get( if with_documents:
title="Musterlösung Fahrzeug" assignment.solution_sample = ContentDocument.objects.get(
) title="Musterlösung Fahrzeug"
)
assignment.save() assignment.save()
LearningContentAssignmentFactory( LearningContentAssignmentFactory(
title="Überprüfen einer Motorfahrzeug-Versicherungspolice", title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
@ -574,13 +640,24 @@ def create_test_circle_reisen(lp):
description="Willkommen im Lehrgang Versicherungsvermitler VBV", description="Willkommen im Lehrgang Versicherungsvermitler VBV",
) )
LearningContentMediaLibraryFactory( LearningContentMediaLibraryFactory(
title=f"Mediathek Reisen", title="Mediathek Reisen",
parent=circle,
content_url="/course/test-lehrgang/media/handlungsfelder/reisen",
)
LearningSequenceFactory(title="Training", parent=circle)
LearningUnitFactory(title="Präsenzkurs", parent=circle)
LearningContentAttendanceCourseFactory(
title="Präsenzkurs Reisen",
parent=circle,
)
LearningUnitFactory(title="Unterlagen", parent=circle)
LearningContentPlaceholderFactory(
title="Unterlagen für den Unterricht",
parent=circle, parent=circle,
content_url=f"/course/test-lehrgang/media/handlungsfelder/reisen",
) )
LearningSequenceFactory(title="Analyse", parent=circle) LearningSequenceFactory(title="Analyse", parent=circle)
# analyse # analyse
lu = LearningUnitFactory( lu = LearningUnitFactory(
title="Bedarfsanalyse, Ist- und Soll-Situation", title="Bedarfsanalyse, Ist- und Soll-Situation",
@ -596,25 +673,27 @@ def create_test_circle_reisen(lp):
content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html", content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html",
) )
LearningContentAssignmentFactory( (
title="Mein Kundenstamm", LearningContentAssignmentFactory(
assignment_type="PRAXIS_ASSIGNMENT", title="Mein Kundenstamm",
parent=circle, assignment_type="PRAXIS_ASSIGNMENT",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-mein-kundenstamm" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
),
), ),
), )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y1"), parent=ActionCompetence.objects.get(competence_id="Y1"),
competence_id=f"Y1.1", competence_id="Y1.1",
title=f"Ich bin fähig zu Reisen eine Gesprächsführung zu machen", title="Ich bin fähig zu Reisen eine Gesprächsführung zu machen",
learning_unit=lu, learning_unit=lu,
) )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y2"), parent=ActionCompetence.objects.get(competence_id="Y2"),
competence_id=f"Y2.1", competence_id="Y2.1",
title=f"Ich bin fähig zu Reisen eine Analyse zu machen", title="Ich bin fähig zu Reisen eine Analyse zu machen",
learning_unit=lu, learning_unit=lu,
) )
@ -627,7 +706,7 @@ def create_test_circle_reisen(lp):
parent=parent, parent=parent,
) )
LearningContentPlaceholderFactory( LearningContentPlaceholderFactory(
title=f"Fachcheck Reisen", title="Fachcheck Reisen",
parent=parent, parent=parent,
) )
LearningContentKnowledgeAssessmentFactory( LearningContentKnowledgeAssessmentFactory(
@ -757,9 +836,9 @@ def create_test_media_library():
die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann. die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.
""".strip(), """.strip(),
body=RichText( body=RichText(
f"<h2>Lernmedien</h2>" "<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>" "<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>" "<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
), ),
) )
@ -778,9 +857,9 @@ def create_test_media_library():
title=cat, title=cat,
parent=media_lib_allgemeines, parent=media_lib_allgemeines,
body=RichText( body=RichText(
f"<h2>Lernmedien</h2>" "<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>" "<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>" "<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
), ),
) )

View File

@ -95,6 +95,8 @@ class CourseConfigurationObjectType(DjangoObjectType):
"enable_circle_documents", "enable_circle_documents",
"enable_learning_mentor", "enable_learning_mentor",
"enable_competence_certificates", "enable_competence_certificates",
"is_uk",
"is_vv",
) )

View File

@ -182,7 +182,7 @@ def command(course):
create_course_uk_it() create_course_uk_it()
if COURSE_TEST_ID in course: if COURSE_TEST_ID in course:
create_test_course(with_sessions=True) create_test_course(with_sessions=True, with_documents=True)
if COURSE_UK_TRAINING in course: if COURSE_UK_TRAINING in course:
create_course_training_de() create_course_training_de()
@ -207,6 +207,8 @@ def create_versicherungsvermittlerin_course(
course_id=course_id, course_id=course_id,
title=names[language], title=names[language],
) )
course.configuration.is_vv = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -354,6 +356,8 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
"it": "Corsi interaziendali", "it": "Corsi interaziendali",
} }
course = create_course_with_categories(course_id=course_id, title=names[lang]) course = create_course_with_categories(course_id=course_id, title=names[lang])
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -387,6 +391,8 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
def create_course_uk_de_course_sessions(): def create_course_uk_de_course_sessions():
course = Course.objects.get(id=COURSE_UK) course = Course.objects.get(id=COURSE_UK)
course.configuration.is_uk = True
course.configuration.save()
cs = CourseSession.objects.create( cs = CourseSession.objects.create(
course_id=COURSE_UK, course_id=COURSE_UK,
@ -500,6 +506,8 @@ def create_course_uk_fr():
course = create_course_with_categories( course = create_course_with_categories(
course_id=COURSE_UK_FR, title="Cours interentreprises" course_id=COURSE_UK_FR, title="Cours interentreprises"
) )
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -550,6 +558,8 @@ def create_course_uk_it():
course = create_course_with_categories( course = create_course_with_categories(
course_id=COURSE_UK_IT, title="Corso interaziendale" course_id=COURSE_UK_IT, title="Corso interaziendale"
) )
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -651,6 +661,8 @@ def create_course_training_de():
course = create_course_with_categories( course = create_course_with_categories(
course_id=COURSE_UK_TRAINING, title="myVBV Training" course_id=COURSE_UK_TRAINING, title="myVBV Training"
) )
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -741,6 +753,8 @@ def create_course_training_fr():
course = create_course_with_categories( course = create_course_with_categories(
course_id=COURSE_UK_TRAINING_FR, title="myVBV Training (FR)" course_id=COURSE_UK_TRAINING_FR, title="myVBV Training (FR)"
) )
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(
@ -828,6 +842,8 @@ def create_course_training_it():
course = create_course_with_categories( course = create_course_with_categories(
course_id=COURSE_UK_TRAINING_IT, title="myVBV Training (IT)" course_id=COURSE_UK_TRAINING_IT, title="myVBV Training (IT)"
) )
course.configuration.is_uk = True
course.configuration.save()
# assignments create assignments parent page # assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory( _assignment_list_page = AssignmentListPageFactory(

View File

@ -0,0 +1,67 @@
# Generated by Django 3.2.20 on 2024-04-03 09:32
from django.db import migrations, models
TEST_COURSE_ID = -1
UK_COURSE_IDS = [
-3, # uk-de
-6, # uk-training-de
-5, # uk-fr
-7, # uk-training-fr
-8, # uk-it
-9, # uk-training-it
]
VV_COURSE_IDS = [
-4, # vv-de
-10, # vv-fr
-11, # vv-it
-12, # vv-prüfung
]
def forward_migration(apps, schema_editor):
Course = apps.get_model("course", "Course")
CourseConfiguration = apps.get_model("course", "CourseConfiguration")
for course in Course.objects.all():
config, created = CourseConfiguration.objects.get_or_create(
course=course,
)
# -> disable unnecessary features
if course.id in UK_COURSE_IDS:
config.is_uk = True
elif course.id in VV_COURSE_IDS:
config.is_vv = True
config.save()
def backward_migration(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("course", "0007_auto_20240226_1553"),
]
operations = [
migrations.AddField(
model_name="courseconfiguration",
name="is_uk",
field=models.BooleanField(default=False, verbose_name="ÜK-Lehrgang"),
),
migrations.AddField(
model_name="courseconfiguration",
name="is_vv",
field=models.BooleanField(
default=False, verbose_name="Versicherungsvermittler-Lehrgang"
),
),
migrations.RunPython(forward_migration, backward_migration),
]

Some files were not shown because too many files have changed in this diff Show More