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">
import AssignmentSubmissionProgress from "@/components/assignment/AssignmentSubmissionProgress.vue";
import type {

View File

@ -24,14 +24,14 @@ const progress = computed(() => ({
<div class="flex items-center">
<i18next :translation="$t('a.NUMBER Elemente abgeschlossen')">
<template #NUMBER>
<span class="mr-3 text-4xl font-bold">{{ totalAssignments }}</span>
<span class="mr-3 text-xl font-bold">{{ totalAssignments }}</span>
</template>
</i18next>
</div>
<div class="flex items-center">
<i18next :translation="$t('a.xOfY Punkten erreicht')">
<template #xOfY>
<span class="mr-3 text-4xl font-bold">
<span class="mr-3 text-xl font-bold">
{{
$t("a.VALUE von MAXIMUM", {
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<{
assignmentsCompleted: number;
avgPassed: number;
courseSlug: string;
}>();
const progress = computed(() => {
@ -19,7 +20,7 @@ const progress = computed(() => {
<template>
<BaseBox
:details-link="'/statistic/assignment'"
:details-link="`/statistic/${courseSlug}/assignment`"
data-cy="dashboard.stats.assignments"
>
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>

View File

@ -6,6 +6,7 @@ import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{
daysCompleted: number;
avgParticipantsPresent: number;
courseSlug: string;
}>();
const progressRecord = computed(() => {
@ -18,7 +19,10 @@ const progressRecord = computed(() => {
</script>
<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 #content>
<div class="flex items-center">

View File

@ -5,7 +5,7 @@ defineProps<{
</script>
<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">
<slot name="title"></slot>
</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 #content>
<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')">
<template #NUMBER>
<span
class="mr-3 text-4xl font-bold"
class="mr-3 text-2xl font-bold"
data-cy="dashboard.stats.competence.success"
>
{{ successCount }}
@ -26,11 +26,13 @@ defineProps<{
</i18next>
</div>
<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')">
<template #NUMBER>
<span
class="mr-3 text-4xl font-bold"
class="mr-3 text-2xl font-bold"
data-cy="dashboard.stats.competence.fail"
>
{{ 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;
statisfactionMax: number;
statisfactionAvg: number;
courseSlug: string;
}>();
const satisfactionColor = computed(() => {
@ -15,7 +16,10 @@ const satisfactionColor = computed(() => {
</script>
<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 #content>
<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">
import { useDashboardStore } from "@/stores/dashboard";
import { computed } from "vue";
import CourseStatistics from "@/components/dashboard/CourseStatistics.vue";
import { computed, onMounted, ref } from "vue";
import AttendanceSummaryBox from "@/components/dashboard/AttendanceSummaryBox.vue";
import type { CourseStatisticsType } from "@/gql/graphql";
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.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 statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const courseSessionSelectionMetrics = computed(() => {
return statistics.value.course_session_selection_metrics;
});
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(() => {
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(() => {
return statistics.value.competences.summary;
return (
statistics?.value?.competences.summary ?? {
fail_total: 0,
success_total: 0,
}
);
});
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>
<template>
<div v-if="statistics" class="mb-14 space-y-8">
<CourseStatistics
:session-count="courseSessionSelectionMetrics.session_count"
: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">
<div v-if="statistics" class="space-y-8">
<div
class="flex flex-col flex-wrap justify-between gap-x-5 border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<AttendanceSummaryBox
class="flex-grow"
:days-completed="attendanceDayPresences.days_completed"
:avg-participants-present="attendanceDayPresences.participants_present"
:course-slug="props.courseSlug"
/>
<AssignmentSummaryBox
class="flex-grow"
:assignments-completed="assigmentSummary.completed_count"
: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
:feedback-count="feebackSummary.total_responses"
:statisfaction-max="feebackSummary.satisfaction_max"
:statisfaction-avg="feebackSummary.satisfaction_average"
:course-slug="props.courseSlug"
/>
<CompetenceSummaryBox
:fail-count="competenceSummary.fail_total"
:success-count="competenceSummary.success_total"
details-link="/statistic/competence"
:details-link="`/statistic/${courseSlug}/competence`"
:course-slug="props.courseSlug"
/>
</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>
import type { CourseSession, DueDate } from "@/types";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useTranslation } from "i18next-vue";
import dayjs from "dayjs";
import type { DashboardDueDate } from "@/services/dashboard";
import { computed } from "vue";
import dayjs from "dayjs";
const props = defineProps<{
dueDate: DueDate;
dueDate: DashboardDueDate;
singleLine?: boolean;
showCourseSession?: boolean;
}>();
const { t } = useTranslation();
const dateType = t(props.dueDate.date_type_translation_key);
const assignmentType = t(props.dueDate.assignment_type_translation_key);
const courseSessionsStore = useCourseSessionsStore();
const courseSession = courseSessionsStore.allCourseSessions.find(
(cs: CourseSession) => cs.id === props.dueDate.course_session_id
);
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 ?? ""
);
const urlText = computed(() => {
let result = "";
if (dateType) {
result += dateType;
}
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>
@ -46,34 +51,42 @@ const courseSessionTitle = computed(() => {
>
<div class="space-y-1">
<div>
<a class="underline" :href="url">
<span class="text-bold">
{{ dayjs(props.dueDate.start).format("D. MMMM YYYY") }}:
<template v-if="dateType">
{{ dateType }}
</template>
<template v-else>
{{ assignmentType }}
</template>
{{ " " }}
<a v-if="showAsUrl" :href="url">
<span class="text-bold text-gray-900">
{{ dayjs(props.dueDate.start).format("dddd D. MMMM YYYY") }}
</span>
<template v-if="assignmentType && dateType">
{{ assignmentType }}:
{{ props.dueDate.title }}
</template>
<template v-else>
{{ props.dueDate.title }}
</template>
</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 class="text-small text-gray-900">
<div>
<span v-if="props.showCourseSession ?? courseSessionTitle">
{{ courseSessionTitle }}:
<span v-if="props.dueDate.course_session.is_uk">
{{ props.dueDate.course_session.session_title }}:
</span>
{{ $t("a.Circle") }} «{{ props.dueDate.circle?.title }}»
</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>
</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 currentCourseSession = courseSessionsStore.currentCourseSession;
if (currentCourseSession) {
return `/course/${currentCourseSession.course.slug}/appointments`;
return `/dashboard/due-dates?session=${currentCourseSession.id}`;
} else {
return `/appointments`;
return `/dashboard/due-dates`;
}
});
@ -121,6 +121,12 @@ const hasLearningMentor = computed(() => {
const courseSession = courseSessionsStore.currentCourseSession;
return courseSession.actions.includes("learning-mentor");
});
const mentorTabTitle = computed(() =>
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
? "a.Praxisbildner"
: "a.Lernbegleitung"
);
</script>
<template>
@ -265,7 +271,7 @@ const hasLearningMentor = computed(() => {
class="nav-item"
:class="{ 'nav-item--active': inLearningMentor() }"
>
{{ t("a.Lernbegleitung") }}
{{ t(mentorTabTitle) }}
</router-link>
</div>
</template>

View File

@ -10,6 +10,8 @@ import {
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
import { computed } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
const router = useRouter();
@ -30,12 +32,20 @@ defineProps<{
const emit = defineEmits(["closemodal", "logout"]);
const courseSessionsStore = useCourseSessionsStore();
const clickLink = (to: string | undefined) => {
if (to) {
router.push(to);
emit("closemodal");
}
};
const mentorTabTitle = computed(() =>
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
? "a.Praxisbildner"
: "a.Lernbegleitung"
);
</script>
<template>
@ -97,7 +107,7 @@ const clickLink = (to: string | undefined) => {
data-cy="navigation-mobile-mentor-link"
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))"
>
{{ $t("a.Lernbegleitung") }}
{{ $t(mentorTabTitle) }}
</button>
</li>

View File

@ -2,9 +2,11 @@
import { useLearningMentees } from "@/services/learningMentees";
import { useCurrentCourseSession } from "@/composables";
import { useCSRFFetch } from "@/fetchHelpers";
import { computed } from "vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const courseSession = useCurrentCourseSession();
const { summary, fetchData } = useLearningMentees(courseSession.value.id);
const { isLoading, summary, fetchData } = useLearningMentees(courseSession.value.id);
const removeMyMentee = async (menteeId: string) => {
await useCSRFFetch(
@ -12,65 +14,69 @@ const removeMyMentee = async (menteeId: string) => {
).delete();
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>
<template>
<div v-if="summary">
<template v-if="summary.participants.length > 0">
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div class="bg-white px-4 py-2">
<div
v-for="participant in summary.participants"
:key="participant.id"
data-cy="lm-my-mentee-list-item"
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"
>
<div class="flex items-center space-x-2">
<img
:alt="participant.last_name"
class="h-11 w-11 rounded-full"
:src="
participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'
"
/>
<div>
<div class="text-bold">
{{ participant.first_name }}
{{ participant.last_name }}
</div>
{{ participant.email }}
<div v-if="isLoading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else>
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div v-if="(summary?.participants?.length ?? 0) > 0" class="bg-white px-4 py-2">
<div
v-for="participant in summary?.participants ?? []"
:key="participant.id"
data-cy="lm-my-mentee-list-item"
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"
>
<div class="flex items-center space-x-2">
<img
:alt="participant.last_name"
class="h-11 w-11 rounded-full"
:src="participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'"
/>
<div>
<div class="text-bold">
{{ participant.first_name }}
{{ participant.last_name }}
</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>
{{ participant.email }}
</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>
</template>
</div>
<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">
<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>

View File

@ -68,19 +68,38 @@ const inviteMentor = async () => {
showInvitationModal.value = false;
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>
<template>
<div v-if="!isLoading" class="bg-gray-200">
<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>
<button
class="btn-secondary flex items-center"
data-cy="lm-invite-mentor-button"
@click="showInvitationModal = true"
>
<it-icon-add class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Neue Lernbegleitung einladen") }}
{{ $t(inviteLearningMentor) }}
</button>
</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">
<it-icon-info class="it-icon mr-2 h-6 w-6 text-sky-700" />
<span>
{{
$t("a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.")
}}
{{ $t(noLearningMentors) }}
</span>
</div>
</div>
</main>
</div>
<ItModal v-model="showInvitationModal">
<template #title>{{ $t("a.Neue Lernbegleitung einladen") }}</template>
<template #title>{{ $t(inviteLearningMentor) }}</template>
<template #body>
<div class="flex flex-col">
<label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label>

View File

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

View File

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

View File

@ -46,7 +46,9 @@ const previousRoute = getPreviousRoute();
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]);

View File

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

View File

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

View File

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

View File

@ -28,104 +28,106 @@ const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
</script>
<template>
<template v-if="isLoaded">
<!-- Self Evaluation -->
<div class="bg-white px-8 py-4 lg:mb-8 lg:py-8">
<div class="mb-8">
<h3 class="mb-4 pb-4 lg:pb-0">
{{ $t("a.Selbsteinschätzungen") }}
</h3>
<ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
<div>
<template v-if="isLoaded">
<!-- Self Evaluation -->
<div class="bg-white px-8 py-4 lg:mb-8 lg:py-8">
<div class="mb-8">
<h3 class="mb-4 pb-4 lg:pb-0">
{{ $t("a.Selbsteinschä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">
<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">
<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"
>
<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"
<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">
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.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">
{{ 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>
<!-- Show All (always)-->
<button
class="btn-text inline-flex items-center py-2 pl-0 pt-8"
@click="emit('showAll')"
>
<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>
<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">
{{ 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>
<span>{{ $t("general.showAll") }}</span>
<it-icon-arrow-right></it-icon-arrow-right>
</button>
</div>
<!-- Show All (always)-->
<button
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>
</div>
</template>
<style scoped></style>

View File

@ -1,12 +1,29 @@
import { useCSRFFetch } from "@/fetchHelpers";
import type { CourseStatisticsType } from "@/gql/graphql";
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 {
circleFlatChildren,
circleFlatLearningContents,
circleFlatLearningUnits,
someFinishedInLearningSequence,
} 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 { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions";
@ -14,11 +31,13 @@ import { useDashboardStore } from "@/stores/dashboard";
import { useUserStore } from "@/stores/user";
import type {
ActionCompetence,
CircleType,
Course,
CourseCompletion,
CourseCompletionStatus,
CourseSession,
CourseSessionDetail,
DashboardPersonsPageMode,
LearningContentWithCompletion,
LearningMentor,
LearningPathType,
@ -26,9 +45,11 @@ import type {
PerformanceCriteria,
} from "@/types";
import { useQuery } from "@urql/vue";
import dayjs from "dayjs";
import { t } from "i18next";
import orderBy from "lodash/orderBy";
import log from "loglevel";
import type { ComputedRef } from "vue";
import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref, watchEffect } from "vue";
export function useCurrentCourseSession() {
@ -166,7 +187,7 @@ export function useCourseData(courseSlug: string) {
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
?.action_competences as ActionCompetence[];
learningPath.value = result.data?.course?.learning_path as LearningPathType;
@ -487,3 +508,223 @@ export function useMyLearningMentors() {
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);
const headers = Object.assign(
@ -56,11 +60,11 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
return response.json().catch(() => {
return Promise.resolve(null);
});
});
}) as Promise<T>;
};
export const itGet = (url: RequestInfo) => {
return itPost(url, {}, { method: "GET" });
export const itGet = <T>(url: RequestInfo) => {
return itPost<T>(url, {}, { method: "GET" });
};
export const itDelete = (url: RequestInfo) => {
@ -81,17 +85,17 @@ export function bustItGetCache(key?: string) {
}
}
export const itGetCached = (
export const itGetCached = <T>(
url: RequestInfo,
options = {
reload: false,
}
): Promise<any> => {
): Promise<T> => {
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({

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 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 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 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 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 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 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 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,
};
@ -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.
*/
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.
*/
@ -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.
*/
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.
*/
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.
*/
@ -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.
*/
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.
*/

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
type Query {
course_statistics(course_id: ID!): CourseStatisticsType
mentor_course_statistics(course_id: ID!): BaseStatisticsType
course_progress(course_id: ID!): CourseProgressType
dashboard_config: [DashboardConfigType!]!
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
@ -20,6 +21,7 @@ type Query {
learning_content_document_list: LearningContentDocumentListObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
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_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_title: String!
course_slug: String!
course_session_properties: StatisticsCourseSessionPropertiesType!
course_session_selection_ids: [ID]!
user_selection_ids: [ID]
assignments: AssignmentsStatisticsType!
course_session_properties: StatisticsCourseSessionPropertiesType!
course_session_selection_metrics: StatisticsCourseSessionsSelectionMetricType!
attendance_day_presences: AttendanceDayPresencesStatisticsType!
feedback_responses: FeedbackStatisticsResponsesType!
assignments: AssignmentsStatisticsType!
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 {
_id: ID!
sessions: [StatisticsCourseSessionDataType!]!
@ -79,13 +125,6 @@ type PresenceRecordStatisticsType {
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 {
_id: ID!
days_completed: Int!
@ -116,40 +155,6 @@ type FeedbackStatisticsSummaryType {
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 {
_id: ID!
summary: CompetencePerformanceStatisticsSummaryType!
@ -173,12 +178,22 @@ type CompetenceRecordStatisticsType {
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 {
_id: ID!
course_id: ID!
session_to_continue_id: ID
competence: ProgressDashboardCompetenceType!
assignment: ProgressDashboardAssignmentType!
competence: ProgressDashboardCompetenceType
assignment: ProgressDashboardAssignmentType
}
type ProgressDashboardCompetenceType {
@ -208,6 +223,7 @@ enum DashboardType {
PROGRESS_DASHBOARD
SIMPLE_DASHBOARD
MENTOR_DASHBOARD
PRAXISBILDNER_DASHBOARD
}
type CourseConfigurationObjectType {
@ -215,6 +231,8 @@ type CourseConfigurationObjectType {
enable_circle_documents: Boolean!
enable_learning_mentor: Boolean!
enable_competence_certificates: Boolean!
is_vv: Boolean!
is_uk: Boolean!
}
type LearningPathObjectType implements CoursePageInterface {

View File

@ -15,6 +15,7 @@ export const AttendanceSummaryStatisticsType = "AttendanceSummaryStatisticsType"
export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus";
export const BaseStatisticsType = "BaseStatisticsType";
export const Boolean = "Boolean";
export const CircleLightObjectType = "CircleLightObjectType";
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(`
query courseSessionDetail($courseSessionId: ID!) {
course_session(id: $courseSessionId) {
@ -220,6 +256,7 @@ export const COURSE_QUERY = graphql(`
enable_circle_documents
enable_learning_mentor
enable_competence_certificates
is_uk
}
action_competences {
competence_id
@ -304,6 +341,7 @@ export const DASHBOARD_CONFIG = graphql(`
enable_circle_documents
enable_learning_mentor
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(`
query courseStatistics($courseId: ID!) {
course_statistics(course_id: $courseId) {
@ -400,6 +448,8 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id
completed_count
average_passed
total_passed
total_failed
}
records {
_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"
@submit.prevent="
userStore.handleLogin(
state.username,
state.password,
state.username.trim(),
state.password.trim(),
route.query.next as string
)
"

View File

@ -40,7 +40,7 @@ const updateItems = async (_items: []) => {
};
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) => {
item.checked = response.includes(item.value);
return item;

View File

@ -73,7 +73,7 @@ function findUserPointsHtml(userId: string) {
"%)";
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"
)}</span>`;
}
@ -157,10 +157,11 @@ function findUserPointsHtml(userId: string) {
</div>
<!-- eslint-disable vue/no-v-html -->
<div
<p
v-if="findGradedUser(csu.user_id) && !isPraxisAssignment"
class="text-left md:text-right"
v-html="findUserPointsHtml(csu.user_id)"
></div>
></p>
</section>
</template>
<template #link>

View File

@ -6,10 +6,10 @@ import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composab
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import log from "loglevel";
import CockpitDates from "@/components/cockpit/CockpitDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
const props = defineProps<{
courseSlug: string;
@ -105,7 +105,11 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</div>
<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>
<SubmissionsOverview
:course-session="courseSession"

View File

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

View File

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

View File

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

View File

@ -1,64 +1,81 @@
<script setup lang="ts">
import log from "loglevel";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed, onMounted } from "vue";
import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables";
import { useCertificateQuery } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
} from "@/pages/competence/utils";
import { useRoute } from "vue-router";
import { getCertificates } from "@/services/competence";
const props = defineProps<{
courseSlug: string;
userId?: string;
}>();
log.debug("CompetenceCertificateListPage setup", props);
const courseSession = useCurrentCourseSession();
const route = useRoute();
const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
},
});
const certificatesQuery = useCertificateQuery(
props.userId,
props.courseSlug
).certificatesQuery;
const competenceCertificates = computed(() => {
const certificates = getCertificates(
certificatesQuery.data.value,
props.userId ?? null
);
if (!certificates) {
return null;
}
return (
(certificatesQuery.data.value?.competence_certificate_list
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
(certificates?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
);
});
const assignments = computed(() => {
return competenceCertificates.value.flatMap((cc) => cc.assignments);
return competenceCertificates?.value?.flatMap((cc) => cc.assignments);
});
const totalPointsEvaluatedAssignments = computed(() => {
return assignmentsMaxEvaluationPoints(assignments.value);
return assignmentsMaxEvaluationPoints(assignments.value ?? []);
});
const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(assignments.value);
return assignmentsUserPoints(assignments.value ?? []);
});
const numAssignmentsEvaluated = computed(() => {
return assignments.value.filter((a) => {
return (assignments.value ?? []).filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED";
}).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 () => {
// log.debug("AssignmentView mounted", props.assignmentId, props.userId);
});
</script>
<template>
<div class="container-large">
<div v-if="assignments" class="container-large">
<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">
@ -92,6 +109,7 @@ onMounted(async () => {
<CompetenceCertificateComponent
:competence-certificate="competenceCertificate"
:detail-view="false"
:frontend-url="certificateFrontendUrl(competenceCertificate.frontend_url)"
></CompetenceCertificateComponent>
</div>
</div>

View File

@ -2,7 +2,7 @@
import * as log from "loglevel";
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import { useCurrentCourseSession } from "@/composables";
import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables";
log.debug("CompetenceParentPage created");
@ -29,6 +29,7 @@ function routeInSelfEvaluationAndFeedback() {
}
const currentCourseSession = useCurrentCourseSession();
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug);
@ -72,7 +73,7 @@ onMounted(async () => {
class="block py-3"
>
{{
currentCourseSession.course.configuration.enable_learning_mentor
hasEvaluationFeedback
? $t("a.Selbst- und Fremdeinschä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">
import type { Component } 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 type { DashboardType } from "@/gql/graphql";
import SimpleCoursePage from "@/pages/dashboard/SimpleCoursePage.vue";
import type { DashboardCourseConfigType } from "@/services/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import CourseDetailDates from "@/components/dashboard/CourseDetailDates.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();
interface DashboardPage {
main: Component;
aside: Component;
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
});
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>
<template>
@ -35,30 +23,24 @@ onMounted(dashboardStore.loadDashboardDetails);
<LoadingSpinner />
</div>
<div
v-else-if="dashboardStore.currentDashboardConfig"
v-else-if="dashboardStore.dashboardConfigsv2.length"
class="flex flex-col lg:flex-row"
>
<main class="grow bg-gray-200 lg:order-2">
<div class="m-8">
<div class="mb-10 flex items-center justify-between">
<h1 data-cy="dashboard-title">Dashboard</h1>
<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>
<component
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main"
></component>
<!-- new way of dashboard -->
<ul>
<li
v-for="config in dashboardStore.dashboardConfigsv2"
:key="config.course_id"
>
<CoursePanel :course-config="newDashboardConfigForId(config.course_id)" />
</li>
</ul>
</div>
</main>
<aside class="m-8 lg:order-1 lg:w-[343px]">
<component
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].aside"
></component>
<aside class="lg:order-2 lg:w-[384px] xl:w-[512px]">
<DashboardAsideWidget />
</aside>
</div>
<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">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type {
AssignmentCompletionMetricsType,
AssignmentStatisticsRecordType,
CourseStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql";
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 dayjs from "dayjs";
import ItProgress from "@/components/ui/ItProgress.vue";
const dashboardStore = useDashboardStore();
const statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const { courseSessionName, circleMeta } = useCourseStatistics();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
courseStatistics: CourseStatisticsType;
courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
if (!metrics.ranking_completed) {
@ -43,20 +39,14 @@ const total = (metrics: AssignmentCompletionMetricsType) => {
</script>
<template>
<main v-if="statistics">
<main>
<div class="mb-10 flex items-center justify-between">
<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 v-if="statistics.assignments.records" class="mt-8 bg-white">
<div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white">
<StatisticFilterList
:course-session-properties="statistics.course_session_properties"
:items="statistics.assignments.records"
:course-session-properties="courseStatistics?.course_session_properties"
:items="courseStatistics.assignments.records"
>
<template #default="{ item }">
<div class="flex justify-between">
@ -89,8 +79,8 @@ const total = (metrics: AssignmentCompletionMetricsType) => {
<div v-else>Noch nicht bestätigt</div>
<ItProgress
:status-count="
assignmentStats((item as AssignmentStatisticsRecordType).metrics)
"
assignmentStats((item as AssignmentStatisticsRecordType).metrics)
"
></ItProgress>
<router-link
class="underline"

View File

@ -1,21 +1,20 @@
<script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type { CourseStatisticsType, PresenceRecordStatisticsType } from "@/gql/graphql";
import type {
CourseStatisticsType,
PresenceRecordStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import { useCourseStatistics } from "@/composables";
import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs";
const dashboardStore = useDashboardStore();
const statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const { courseSessionName, circleMeta } = useCourseStatistics();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
courseStatistics: CourseStatisticsType;
courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const attendanceStats = (present: number, total: number) => {
return {
@ -27,20 +26,17 @@ const attendanceStats = (present: number, total: number) => {
</script>
<template>
<main v-if="statistics">
<main>
<div class="mb-10 flex items-center justify-between">
<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 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
:course-session-properties="statistics.course_session_properties"
:items="statistics.attendance_day_presences.records"
:course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.attendance_day_presences.records"
>
<template #default="{ item }">
<div class="flex justify-between">

View File

@ -1,38 +1,28 @@
<script setup lang="ts">
import { useDashboardStore } from "@/stores/dashboard";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed } from "vue";
import type {
CompetenceRecordStatisticsType,
CourseStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { useCourseStatistics } from "@/composables";
const dashboardStore = useDashboardStore();
const statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const { courseSessionName, circleMeta } = useCourseStatistics();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
courseStatistics: CourseStatisticsType;
courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
</script>
<template>
<main v-if="statistics">
<main>
<div class="mb-10 flex items-center justify-between">
<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 v-if="statistics.competences.records" class="mt-8 bg-white">
<div v-if="courseStatistics?.competences.records" class="mt-8 bg-white">
<StatisticFilterList
:course-session-properties="statistics.course_session_properties"
:items="statistics.competences.records"
:course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.competences.records"
>
<template #default="{ item }">
<div class="flex justify-between">

View File

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

View File

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

View File

@ -29,7 +29,19 @@ onMounted(() => {
>
<ul class="flex flex-col lg:flex-row">
<li
data-cy="lm-mentees-navigation-link"
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="{
'border-b-2 border-b-blue-900': route.name
?.toString()
@ -41,19 +53,7 @@ onMounted(() => {
:to="{ name: 'learningMentorOverview' }"
class="block py-3"
>
{{ $t("a.Übersicht") }}
</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") }}
{{ $t("a.Aufgaben") }}
</router-link>
</li>
</ul>

View File

@ -10,8 +10,11 @@ const isMyMentorsVisible = computed(() =>
courseSession.value.actions.includes("learning-mentor::edit-mentors")
);
const isMyMenteesVisible = computed(() =>
courseSession.value.actions.includes("learning-mentor::guide-members")
const isMyMenteesVisible = computed(
() =>
courseSession.value.actions.includes("learning-mentor::guide-members") ||
courseSession.value.actions.includes("is_expert") ||
courseSession.value.actions.includes("is_supervisor")
);
</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(() => {
if (lpQueryResult.course.value?.configuration.enable_learning_mentor) {
if (expertAsContact.value) {
return {
contactDescription: "circlePage.contactLearningMentorDescription",
contactButton: "circlePage.contactLearningMentorButton",
@ -98,7 +105,7 @@ interface Mentor {
const experts = computed<Expert[] | null>(() => {
if (courseConfig.value.showContact) {
if (lpQueryResult.course.value?.configuration.enable_learning_mentor) {
if (expertAsContact.value) {
if (mentors.value?.length > 0) {
return mentors.value.map((m: Mentor) => m.mentor);
}

View File

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

View File

@ -22,7 +22,7 @@ const documents = ref<BlockDocument[]>([]);
onMounted(async () => {
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;
});
</script>

View File

@ -64,7 +64,7 @@ async function startTest() {
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,
extended_time_test: extendedTimeTest.value,
});

View File

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

View File

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

View File

@ -1,63 +1,102 @@
<script setup lang="ts">
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";
type SubMenuType = "OVERVIEW" | "DETAILS";
import { useRoute } from "vue-router";
const props = defineProps<{
userId: string;
courseSlug: string;
certificateSlug?: string;
}>();
interface SubMenuItem {
type: SubMenuType;
label: string;
url: string;
inMenu: boolean;
routeMatch: string[];
}
const MENU_ENTRIES: SubMenuItem[] = [
{ type: "OVERVIEW", label: "a.Übersicht" },
const SUBPAGES: SubMenuItem[] = [
{
type: "DETAILS",
label: useCurrentCourseSession().value.course.configuration.enable_learning_mentor
label: "a.Übersicht",
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.Selbsteinschätzungen",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/evaluations`,
inMenu: true,
routeMatch: ["competenceEvaluations"],
},
];
const active = ref<SubMenuItem>(MENU_ENTRIES[0]);
const selectDetails = () => {
active.value = MENU_ENTRIES[1];
};
if (useCurrentCourseSession().value.course.configuration.is_uk) {
SUBPAGES.push(
{
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>
<template>
<CockpitProfileContent>
<template #side>
<div v-for="(entry, index) in MENU_ENTRIES" :key="index" class="mb-2">
<button
<div
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="{ 'text-bold bg-gray-200': active.type === entry.type }"
@click="active = entry"
:class="{
'text-bold bg-gray-200': route.matched.some((record) =>
entry.routeMatch.includes(convertRouteRecordNameToString(record?.name))
),
}"
>
<span>{{ $t(entry.label) }}</span>
</button>
</router-link>
</div>
</template>
<template #main>
<div class="container-large">
<SelfEvaluationAndFeedbackOverview
v-if="active.type === 'OVERVIEW'"
<router-view
:profile-user-id="props.userId"
@show-all="selectDetails"
/>
<SelfEvaluationAndFeedbackList
v-else-if="active.type === 'DETAILS'"
class="w-full"
:profile-user-id="props.userId"
/>
:user-id="props.userId"
:course-slug="useCurrentCourseSession().value.course.slug"
:certificate-slug="certificateSlug ? certificateSlug : ''"
></router-view>
</div>
</template>
</CockpitProfileContent>

View File

@ -13,8 +13,16 @@ const props = defineProps<{
const { t } = useTranslation();
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();
@ -54,7 +62,11 @@ onMounted(() => {
v-for="page in pages"
:key="page.route"
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 }">
{{ page.label }}

View File

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

View File

@ -7,19 +7,32 @@ import type {
} from "vue-router";
const routeHistory: RouteLocationNormalized[] = [];
const MAX_HISTORY = 10; // for example, store the last 10 visited routes
const MAX_HISTORY = 10;
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) => {
// Add the current route to the history, and ensure it doesn't exceed the maximum length
if (isFirstNavigation) {
isFirstNavigation = false;
} else {
} else if (lastNavigationWasPush) {
routeHistory.push(from);
}
if (routeHistory.length > MAX_HISTORY) {
routeHistory.shift();
}
lastNavigationWasPush = false;
next();
};

View File

@ -10,7 +10,7 @@ import {
redirectToLoginIfRequired,
updateLoggedIn,
} from "@/router/guards";
import { addToHistory } from "@/router/history";
import { addToHistory, setLastNavigationWasPush } from "@/router/history";
import { onboardingRedirect } from "@/router/onboarding";
import { createRouter, createWebHistory } from "vue-router";
@ -60,6 +60,19 @@ const router = createRouter({
name: "home",
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",
props: true,
@ -166,6 +179,37 @@ const router = createRouter({
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
props: true,
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: [
{
path: "",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "participants",
component: () =>
import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"),
name: "mentorsAndParticipants",
},
{
path: "tasks",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "self-evaluation-feedback/:learningUnitId",
component: () =>
@ -264,7 +308,7 @@ const router = createRouter({
],
},
{
path: "/statistic",
path: "/statistic/:courseSlug",
props: true,
component: () => import("@/pages/dashboard/statistic/StatisticParentPage.vue"),
children: [
@ -316,14 +360,6 @@ const router = createRouter({
path: "/notifications",
component: () => import("@/pages/NotificationsPage.vue"),
},
{
path: "/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{
path: "/course/:courseSlug/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{
path: "/onboarding/:courseType",
props: true,
@ -387,9 +423,26 @@ router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired);
// register after login hooks
router.beforeEach(handleCurrentCourseSession);
router.beforeEach(handleCourseSessionAsQueryParam);
router.beforeEach(async (to) => await handleCurrentCourseSession(to));
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);
// 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;

View File

@ -1,3 +1,8 @@
import type {
CompetenceCertificateForUserQueryQuery,
CompetenceCertificateListObjectType,
CompetenceCertificateQueryQuery,
} from "@/gql/graphql";
import type { PerformanceCriteria } from "@/types";
import groupBy from "lodash/groupBy";
@ -17,3 +22,42 @@ export function calcPerformanceCriteriaStatusCount(criteria: PerformanceCriteria
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_COURSE_SESSION_PROGRESS,
DASHBOARD_COURSE_STATISTICS,
DASHBOARD_MENTOR_COMPETENCE_SUMMARY,
} from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import type {
AssignmentsStatisticsType,
CourseProgressType,
CourseStatisticsType,
DashboardConfigType,
} 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 (
courseId: string
@ -47,6 +127,7 @@ export const fetchProgressData = async (
return null;
}
};
export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => {
try {
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);
}
return res.data?.dashboard_config || null;
return (res.data?.dashboard_config as unknown as DashboardConfigType[]) || null;
} catch (error) {
console.error("Error fetching dashboard config:", error);
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 organisations: Ref<Organisation[]> = ref([]);
itGetCached("/api/core/entities/").then((res) => {
itGetCached("/api/core/entities/").then((res: any) => {
countries.value = res.countries;
organisations.value = res.organisations;
});

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import { itGetCached } from "@/fetchHelpers";
import type { CourseSession, DueDate } from "@/types";
import type { CourseSession } from "@/types";
import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs";
import uniqBy from "lodash/uniqBy";
import log from "loglevel";
import { defineStore } from "pinia";
@ -25,13 +24,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
const userStore = useUserStore();
if (userStore.loggedIn) {
// TODO: refactor after implementing of Klassenkonzept
await Promise.all(
allCourseSessions.value.map(async (cs) => {
sortDueDates(cs.due_dates);
})
);
if (!allCourseSessions.value) {
throw `No courseSessionData found for user`;
}
@ -137,37 +129,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
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 {
uniqueCourseSessionsByCourse,
allCurrentCourseSessions,
getCourseSessionById,
switchCourseSessionById,
isCourseSessionPreviewActive,
allDueDates,
// use `useCurrentCourseSession` whenever possible
currentCourseSession,

View File

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

View File

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

View File

@ -27,7 +27,7 @@ export const useNotificationsStore = defineStore("notifications", () => {
}
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;
}

View File

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

View File

@ -193,6 +193,8 @@ export interface CourseConfiguration {
enable_circle_documents: boolean;
enable_learning_mentor: boolean;
enable_competence_certificates: boolean;
is_uk: boolean;
is_vv: boolean;
}
export interface Course {
@ -207,6 +209,8 @@ export interface CourseCategory {
id: string;
name: string;
general: boolean;
is_uk: boolean;
is_vv: boolean;
}
export type MediaLibraryContentBlockValue = {
@ -451,7 +455,6 @@ export interface CourseSession {
title: string;
start_date: string;
end_date: string;
due_dates: DueDate[];
actions: string[];
}
@ -607,3 +610,5 @@ export type User = {
course_session_experts: any[];
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");
cy.visit("/course/test-lehrgang/learn/reisen");
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9);
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 4);
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");
// learning unit id = 687 also known as:
// learning unit id = 692 also known as:
// Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>>
const identifier = "self-eval-687"
const identifier = "self-eval-692"
// data in KompetenzNavi/Selbsteinschätzungen is correct
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", () => {
clickOnDetailsLink("attendance");
cy.url().should("contain", "/statistic/attendance");
cy.url().should("contain", "/statistic/test-lehrgang/attendance");
// might be improved: roughly check
// 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", () => {
it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3");
@ -78,7 +70,7 @@ describe("dashboardSupervisor.cy.js", () => {
});
it("contains correct details link", () => {
clickOnDetailsLink("feedback");
cy.url().should("contain", "/statistic/feedback");
cy.url().should("contain", "/statistic/test-lehrgang/feedback");
// might be improved: roughly check
// that the correct data is displayed
@ -96,7 +88,7 @@ describe("dashboardSupervisor.cy.js", () => {
});
it("contains correct details link", () => {
clickOnDetailsLink("competence");
cy.url().should("contain", "/statistic/competence");
cy.url().should("contain", "/statistic/test-lehrgang/competence");
// might be improved: roughly check
// 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_MENTEES_URL = "/course/versicherungsvermittler-in/learning-mentor/participants";
export const MENTOR_TASKS_URL_VV =
"/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 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_OVERVIEW_NAVIGATION_LINK = "[data-cy=lm-overview-navigation-link]";
export const MENTOR_MENTEES_NAVIGATION_LINK = "[data-cy=lm-mentees-navigation-link]";
export const MENTOR_OVERVIEW_NAVIGATION_LINK =
"[data-cy=lm-overview-navigation-link]";
export const MENTOR_MENTEES_NAVIGATION_LINK =
"[data-cy=lm-mentees-navigation-link]";
// /participants
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_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 {
MAIN_NAVIGATION_MENTOR_LINK,
MEMBER_DASHBOARD_LINK,
MENTEE_INVITE_MENTOR,
MENTEE_MENTOR_LIST_ITEM,
MENTEE_MENTOR_REMOVE,
MENTEE_MENTORS_TITLE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MENTEES_URL_UK,
MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
MENTOR_TASKS_URL_VV,
} from "../constants";
describe("memberOnly.cy.js", () => {
@ -23,22 +27,22 @@ describe("memberOnly.cy.js", () => {
});
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");
})
});
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");
})
});
it("shows NO mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("not.exist");
});
it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("exist");
});
@ -47,12 +51,44 @@ describe("memberOnly.cy.js", () => {
const mentor = "Micheala Weber-Mentor";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE)
.click();
// then
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 {
MEMBER_DASHBOARD_LINK,
MENTEE_MENTOR_LIST_ITEM,
@ -8,11 +8,11 @@ import {
MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
MENTOR_TASKS_URL_VV,
} from "../constants";
describe("mentorAndMember.cy.js", () => {
@ -27,34 +27,34 @@ describe("mentorAndMember.cy.js", () => {
});
it("shows the learning mentor navigation", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.visit(MENTOR_TASKS_URL_VV);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
});
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.url().should("include", MENTOR_MENTEES_URL);
})
cy.url().should("include", MENTOR_MENTEES_URL_VV);
});
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.url().should("include", MENTOR_OVERVIEW_URL);
})
cy.url().should("include", MENTOR_TASKS_URL_VV);
});
it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("exist");
});
it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("exist");
});
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");
});
@ -63,50 +63,55 @@ describe("mentorAndMember.cy.js", () => {
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE)
.click();
// 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.contains(mentee).should("exist");
})
});
it("can remove a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE)
.click();
// then
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", () => {
const mentor = "Micheala Weber-Mentor";
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("contain", mentor);
})
});
it("can remove a mentor", () => {
// given
const mentor = "Micheala Weber-Mentor";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE)
.click();
// then
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 {
MENTOR_DASHBOARD_LINK,
MENTOR_MAIN_NAVIGATION,
@ -6,11 +6,11 @@ import {
MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MENTEES_URL_VV,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
MENTOR_TASKS_URL_VV,
} from "../constants";
describe("mentorOnly.cy.js", () => {
@ -22,38 +22,38 @@ describe("mentorOnly.cy.js", () => {
it("shows the correct dashboard", () => {
cy.visit("/");
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", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
});
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.url().should("include", MENTOR_MENTEES_URL);
})
cy.url().should("include", MENTOR_MENTEES_URL_VV);
});
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.url().should("include", MENTOR_OVERVIEW_URL);
})
cy.url().should("include", MENTOR_TASKS_URL_VV);
});
it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTEES).should("exist");
});
it("shows no mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MENTOR_MY_MENTORS).should("not.exist");
});
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", "Viktor Vollgas");
});
@ -63,29 +63,32 @@ describe("mentorOnly.cy.js", () => {
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE)
.click();
// 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.contains(mentee).should("exist");
})
});
it("can remove a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.visit(MENTOR_MENTEES_URL_VV);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE)
.click();
// then
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("#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.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", () => {
login("test-student1@example.com", "test");
cy.visit("/");
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", () => {
@ -36,9 +38,9 @@ describe("login.cy.js", () => {
cy.get("#username").type("test-student1@example.com");
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",
"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,
)
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 (
export_students,
export_students_and_trainers,
@ -116,6 +123,14 @@ urlpatterns = [
re_path(r"api/notify/email_notification_settings/$", 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
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
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
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 = (
CoursePage.objects.get(course_id=course_id)
.get_children()
@ -40,7 +42,6 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True,
competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace(
"""
<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_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.append(
@ -3591,7 +3597,7 @@ def create_uk_reflection(course_id=COURSE_UK):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="ca. 1 Stunde",
intro_text=replace_whitespace(
"""
@ -3747,7 +3753,7 @@ def create_uk_fr_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="",
intro_text=replace_whitespace(
"""
@ -3900,7 +3906,7 @@ def create_uk_it_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Riflessione",
title="Riflessione",
effort_required="",
intro_text=replace_whitespace(
"""
@ -4053,7 +4059,7 @@ def create_vv_reflection(
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="ca. 1 Stunde",
intro_text=replace_whitespace(
"""

View File

@ -101,6 +101,10 @@ class AssignmentObjectType(DjangoObjectType):
lp = self.find_attached_learning_content()
if lp:
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(
info=info,
course_session_id=course_session_id,

View File

@ -9,6 +9,8 @@ from vbv_lernwelt.competence.models import (
CompetenceCertificateList,
)
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):
@ -24,6 +26,15 @@ class CompetenceCertificateQuery(object):
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):
return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug)
@ -39,3 +50,26 @@ class CompetenceCertificateQuery(object):
course_id=course_id,
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