Merged develop into feature/vueuse-update

This commit is contained in:
Christian Cueni 2024-05-06 06:25:04 +00:00
commit a59e1689e3
131 changed files with 5406 additions and 1411 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,170 @@
<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 === "Supervisor") {
return {
href: getCockpitUrl(props.courseConfig?.course_slug),
text: "Cockpit anschauen",
cyKey: "cockpit-dashboard-link",
};
}
if (props.courseConfig?.role_key === "Trainer") {
return {
href: getCockpitUrl(props.courseConfig?.course_slug),
text: "Cockpit anschauen",
cyKey: "cockpit-dashboard-link",
};
}
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

@ -59,9 +59,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 +121,12 @@ 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 = computed(() =>
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
? "a.Praxisbildner"
: "a.Lernbegleitung"
);
</script> </script>
<template> <template>
@ -265,7 +271,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,8 @@ import {
getLearningPathUrl, getLearningPathUrl,
getMediaCenterUrl, getMediaCenterUrl,
} from "@/utils/utils"; } from "@/utils/utils";
import { computed } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
const router = useRouter(); const router = useRouter();
@ -30,12 +32,20 @@ defineProps<{
const emit = defineEmits(["closemodal", "logout"]); const emit = defineEmits(["closemodal", "logout"]);
const courseSessionsStore = useCourseSessionsStore();
const clickLink = (to: string | undefined) => { const clickLink = (to: string | undefined) => {
if (to) { if (to) {
router.push(to); router.push(to);
emit("closemodal"); emit("closemodal");
} }
}; };
const mentorTabTitle = computed(() =>
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
? "a.Praxisbildner"
: "a.Lernbegleitung"
);
</script> </script>
<template> <template>
@ -97,7 +107,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

@ -2,9 +2,11 @@
import { useLearningMentees } from "@/services/learningMentees"; import { useLearningMentees } from "@/services/learningMentees";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { useCSRFFetch } from "@/fetchHelpers"; import { useCSRFFetch } from "@/fetchHelpers";
import { computed } from "vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const { summary, fetchData } = useLearningMentees(courseSession.value.id); const { isLoading, summary, fetchData } = useLearningMentees(courseSession.value.id);
const removeMyMentee = async (menteeId: string) => { const removeMyMentee = async (menteeId: string) => {
await useCSRFFetch( await useCSRFFetch(
@ -12,65 +14,69 @@ const removeMyMentee = async (menteeId: string) => {
).delete(); ).delete();
fetchData(); fetchData();
}; };
const noMenteesText = computed(() =>
courseSession.value.course.configuration.is_uk
? "a.Aktuell begleitest du niemanden als Praxisbildner."
: "a.Aktuell begleitest du niemanden als Lernbegleitung."
);
</script> </script>
<template> <template>
<div v-if="summary"> <div v-if="isLoading" class="m-8 flex justify-center">
<template v-if="summary.participants.length > 0"> <LoadingSpinner />
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2> </div>
<div class="bg-white px-4 py-2"> <div v-else>
<div <h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
v-for="participant in summary.participants" <div v-if="(summary?.participants?.length ?? 0) > 0" class="bg-white px-4 py-2">
:key="participant.id" <div
data-cy="lm-my-mentee-list-item" v-for="participant in summary?.participants ?? []"
class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16" :key="participant.id"
> data-cy="lm-my-mentee-list-item"
<div class="flex items-center space-x-2"> class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16"
<img >
:alt="participant.last_name" <div class="flex items-center space-x-2">
class="h-11 w-11 rounded-full" <img
:src=" :alt="participant.last_name"
participant.avatar_url || '/static/avatars/myvbv-default-avatar.png' class="h-11 w-11 rounded-full"
" :src="participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'"
/> />
<div> <div>
<div class="text-bold"> <div class="text-bold">
{{ participant.first_name }} {{ participant.first_name }}
{{ participant.last_name }} {{ participant.last_name }}
</div>
{{ participant.email }}
</div> </div>
</div> {{ participant.email }}
<div class="space-x-5">
<router-link
data-cy="lm-my-mentee-profile"
:to="{
name: 'profileLearningPath',
params: {
userId: participant.id,
courseSlug: courseSession.course.slug,
},
}"
class="underline"
>
{{ $t("cockpit.profileLink") }}
</router-link>
<button
class="underline"
data-cy="lm-my-mentee-remove"
@click="removeMyMentee(participant.id)"
>
{{ $t("a.Entfernen") }}
</button>
</div> </div>
</div> </div>
<div class="space-x-5">
<router-link
data-cy="lm-my-mentee-profile"
:to="{
name: 'profileLearningPath',
params: {
userId: participant.id,
courseSlug: courseSession.course.slug,
},
}"
class="underline"
>
{{ $t("cockpit.profileLink") }}
</router-link>
<button
class="underline"
data-cy="lm-my-mentee-remove"
@click="removeMyMentee(participant.id)"
>
{{ $t("a.Entfernen") }}
</button>
</div>
</div> </div>
</template> </div>
<div v-else> <div v-else>
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div class="flex items-center bg-white px-4 py-2"> <div class="flex items-center bg-white px-4 py-2">
<it-icon-info class="it-icon mr-2 h-6 w-6" /> <it-icon-info class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Aktuell begleitest du niemanden als Lernbegleitung.") }} {{ $t(noMenteesText) }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -68,19 +68,38 @@ const inviteMentor = async () => {
showInvitationModal.value = false; showInvitationModal.value = false;
inviteeEmail.value = ""; inviteeEmail.value = "";
}; };
const myLearningMentors = computed(() =>
courseSession.value.course.configuration.is_uk
? "Meine Praxisbildner"
: "Meine Lernbegleiter"
);
const inviteLearningMentor = computed(() =>
courseSession.value.course.configuration.is_uk
? "Neuen Praxisbildner einladen"
: "a.Neue Lernbegleitung einladen"
);
const noLearningMentors = computed(() =>
courseSession.value.course.configuration.is_uk
? "a.Aktuell hast du noch keine Person als Praxisbildner eingeladen."
: "a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen."
);
</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 +162,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,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { computed } from "vue";
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const actionNoLearningMentors = computed(() =>
currentCourseSession.value.course.configuration.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."
);
const inviteLearningMentorShort = computed(() =>
currentCourseSession.value.course.configuration.is_uk
? "Neuen Praxisbildner einladen"
: "a.Neue Lernbegleitung einladen"
);
</script> </script>
<template> <template>
@ -9,11 +22,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 +31,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

@ -46,7 +46,9 @@ const previousRoute = getPreviousRoute();
const learningUnitHasFeedbackPage = computed( const learningUnitHasFeedbackPage = computed(
() => () =>
courseSession.value.course.configuration.enable_learning_mentor && !isReadOnly.value courseSession.value.course.configuration.enable_learning_mentor &&
!courseSession.value.course.configuration.is_uk &&
!isReadOnly.value
); );
const currentQuestion = computed(() => questions.value[questionIndex.value]); const currentQuestion = computed(() => questions.value[questionIndex.value]);

View File

@ -5,6 +5,7 @@ import SmileyCell from "@/components/selfEvaluationFeedback/SmileyCell.vue";
const props = defineProps<{ const props = defineProps<{
summary: LearningUnitSummary; summary: LearningUnitSummary;
hideDetailLink?: boolean;
}>(); }>();
const hasFeedbackReceived = computed(() => { const hasFeedbackReceived = computed(() => {
@ -34,6 +35,7 @@ const feedbackProviderName = computed(() => {
</div> </div>
<span class="pl-4 underline"> <span class="pl-4 underline">
<router-link <router-link
v-if="!props.hideDetailLink"
:to="props.summary.detail_url" :to="props.summary.detail_url"
:data-cy="`self-eval-${summary.id}-detail-url`" :data-cy="`self-eval-${summary.id}-detail-url`"
> >

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

@ -1,22 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback"; import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import FeedbackByLearningUnitSummary from "@/components/selfEvaluationFeedback/FeedbackByLearningUnitSummary.vue"; import FeedbackByLearningUnitSummary from "@/components/selfEvaluationFeedback/FeedbackByLearningUnitSummary.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { t } from "i18next"; import { t } from "i18next";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
profileUserId: string; profileUserId: string;
}>(); }>();
const userStore = useUserStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries( const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
courseSession.value.id, courseSession.value.id,
props.profileUserId props.profileUserId
); );
const course = computed(() => courseSession.value.course);
const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value); const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
const selectedCircle = ref({ name: t("a.AlleCircle"), id: "_all" }); const selectedCircle = ref({ name: t("a.AlleCircle"), id: "_all" });
@ -37,13 +38,19 @@ const summaries = computed(() => {
); );
}); });
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
const headerTitle = computed(() => { const headerTitle = computed(() => {
if (course.value.configuration.enable_learning_mentor) { if (hasEvaluationFeedback.value) {
return t("a.Selbst- und Fremdeinschätzungen"); return t("a.Selbst- und Fremdeinschätzungen");
} else { } else {
return t("a.Selbsteinschätzungen"); return t("a.Selbsteinschätzungen");
} }
}); });
const isOwnEvaluation = computed(() => {
return props.profileUserId === userStore.id;
});
</script> </script>
<template> <template>
@ -63,6 +70,7 @@ const headerTitle = computed(() => {
v-for="summary in summaries" v-for="summary in summaries"
:key="summary.id" :key="summary.id"
:summary="summary" :summary="summary"
:hide-detail-link="!isOwnEvaluation"
/> />
</div> </div>
</div> </div>

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,223 @@ 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 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 };
}
export function useEvaluationWithFeedback() {
const currentCourseSession = useCurrentCourseSession();
const hasFeedback = computed(
() =>
currentCourseSession.value.course.configuration.enable_learning_mentor &&
!currentCourseSession.value.course.configuration.is_uk
);
return { hasFeedback };
}

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,38 +1,38 @@
<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 { computed } from "vue";
import { useQuery } from "@urql/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(() => {
return ( const certificates = getCertificates(
(certificatesQuery.data.value?.competence_certificate_list certificatesQuery.data.value,
?.competence_certificates as unknown as CompetenceCertificate[]) ?? [] props.userId ?? null
).find((cc) => cc.slug.endsWith(props.certificateSlug)); );
});
onMounted(async () => { if (!certificates) {
// log.debug("AssignmentView mounted", props.assignmentId, props.userId); return null;
}
return (
(certificates.competence_certificates as unknown as CompetenceCertificate[]) ?? []
).find((cc) => cc.slug.endsWith(props.certificateSlug));
}); });
</script> </script>
@ -40,11 +40,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

@ -2,7 +2,7 @@
import * as log from "loglevel"; import * as log from "loglevel";
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables";
log.debug("CompetenceParentPage created"); log.debug("CompetenceParentPage created");
@ -29,6 +29,7 @@ function routeInSelfEvaluationAndFeedback() {
} }
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
onMounted(async () => { onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug); log.debug("CompetenceParentPage mounted", props.courseSlug);
@ -72,7 +73,7 @@ onMounted(async () => {
class="block py-3" class="block py-3"
> >
{{ {{
currentCourseSession.course.configuration.enable_learning_mentor hasEvaluationFeedback
? $t("a.Selbst- und Fremdeinschätzungen") ? $t("a.Selbst- und Fremdeinschätzungen")
: $t("a.Selbsteinschätzungen") : $t("a.Selbsteinschätzungen")
}} }}

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

@ -10,8 +10,11 @@ const isMyMentorsVisible = computed(() =>
courseSession.value.actions.includes("learning-mentor::edit-mentors") courseSession.value.actions.includes("learning-mentor::edit-mentors")
); );
const isMyMenteesVisible = computed(() => const isMyMenteesVisible = computed(
courseSession.value.actions.includes("learning-mentor::guide-members") () =>
courseSession.value.actions.includes("learning-mentor::guide-members") ||
courseSession.value.actions.includes("is_expert") ||
courseSession.value.actions.includes("is_supervisor")
); );
</script> </script>

View File

@ -63,8 +63,15 @@ const showDocumentSection = computed(() => {
); );
}); });
const expertAsContact = computed(() => {
return (
lpQueryResult.course.value?.configuration.enable_learning_mentor &&
lpQueryResult.course.value?.configuration.is_vv
);
});
const courseConfig = computed(() => { const courseConfig = computed(() => {
if (lpQueryResult.course.value?.configuration.enable_learning_mentor) { if (expertAsContact.value) {
return { return {
contactDescription: "circlePage.contactLearningMentorDescription", contactDescription: "circlePage.contactLearningMentorDescription",
contactButton: "circlePage.contactLearningMentorButton", contactButton: "circlePage.contactLearningMentorButton",
@ -98,7 +105,7 @@ interface Mentor {
const experts = computed<Expert[] | null>(() => { const experts = computed<Expert[] | null>(() => {
if (courseConfig.value.showContact) { if (courseConfig.value.showContact) {
if (lpQueryResult.course.value?.configuration.enable_learning_mentor) { if (expertAsContact.value) {
if (mentors.value?.length > 0) { if (mentors.value?.length > 0) {
return mentors.value.map((m: Mentor) => m.mentor); return mentors.value.map((m: Mentor) => m.mentor);
} }

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
@ -109,7 +116,9 @@ export async function handleAcceptLearningMentorInvitation(
return; return;
} }
return `/onboarding/vv-${user.language}/account/create?next=${encodeURIComponent( const redirectCourse = to.query.uk ? `uk` : `vv-${user.language}`;
return `/onboarding/${redirectCourse}/account/create?next=${encodeURIComponent(
to.fullPath to.fullPath
)}`; )}`;
} }

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"
| "Trainer"
| "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)

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