Add new DashboardDueDatesPage
This commit is contained in:
parent
50c35b7100
commit
102196a290
|
|
@ -2,8 +2,8 @@
|
||||||
import type { CourseSession, DueDate } from "@/types";
|
import type { CourseSession, DueDate } from "@/types";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dueDate: DueDate;
|
dueDate: DueDate;
|
||||||
|
|
@ -48,24 +48,23 @@ const courseSessionTitle = computed(() => {
|
||||||
<div>
|
<div>
|
||||||
<a class="underline" :href="url">
|
<a class="underline" :href="url">
|
||||||
<span class="text-bold">
|
<span class="text-bold">
|
||||||
{{ dayjs(props.dueDate.start).format("D. MMMM YYYY") }}:
|
{{ dayjs(props.dueDate.start).format("dddd D. MMMM YYYY") }}
|
||||||
<template v-if="dateType">
|
|
||||||
{{ dateType }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ assignmentType }}
|
|
||||||
</template>
|
|
||||||
{{ " " }}
|
|
||||||
</span>
|
</span>
|
||||||
<template v-if="assignmentType && dateType">
|
|
||||||
{{ assignmentType }}:
|
|
||||||
{{ props.dueDate.title }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ props.dueDate.title }}
|
|
||||||
</template>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div v-if="dateType">
|
||||||
|
{{ dateType }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="assignmentType && !props.dueDate.title.startsWith(assignmentType)">
|
||||||
|
{{ assignmentType }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.dueDate.title">
|
||||||
|
{{ props.dueDate.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="text-small text-gray-900">
|
<div class="text-small text-gray-900">
|
||||||
<div>
|
<div>
|
||||||
<span v-if="props.showCourseSession ?? courseSessionTitle">
|
<span v-if="props.showCourseSession ?? courseSessionTitle">
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ import {
|
||||||
circleFlatLearningUnits,
|
circleFlatLearningUnits,
|
||||||
someFinishedInLearningSequence,
|
someFinishedInLearningSequence,
|
||||||
} from "@/services/circle";
|
} from "@/services/circle";
|
||||||
import type { DashboardPersonType } from "@/services/dashboard";
|
import type { DashboardDueDate, DashboardPersonType } from "@/services/dashboard";
|
||||||
import { fetchDashboardPersons, fetchStatisticData } from "@/services/dashboard";
|
import {
|
||||||
|
fetchDashboardDueDates,
|
||||||
|
fetchDashboardPersons,
|
||||||
|
fetchStatisticData,
|
||||||
|
} from "@/services/dashboard";
|
||||||
import { presignUpload, uploadFile } from "@/services/files";
|
import { presignUpload, uploadFile } from "@/services/files";
|
||||||
import { useCompletionStore } from "@/stores/completion";
|
import { useCompletionStore } from "@/stores/completion";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
|
@ -30,6 +34,7 @@ import type {
|
||||||
PerformanceCriteria,
|
PerformanceCriteria,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { useQuery } from "@urql/vue";
|
import { useQuery } from "@urql/vue";
|
||||||
|
import { t } from "i18next";
|
||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
import type { ComputedRef, Ref } from "vue";
|
||||||
|
|
@ -494,12 +499,28 @@ export function useMyLearningMentors() {
|
||||||
|
|
||||||
export function useDashboardPersons() {
|
export function useDashboardPersons() {
|
||||||
const dashboardPersons = ref<DashboardPersonType[]>([]);
|
const dashboardPersons = ref<DashboardPersonType[]>([]);
|
||||||
|
const dashboardDueDates = ref<DashboardDueDate[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
dashboardPersons.value = await fetchDashboardPersons();
|
const [persons, dueDates] = await Promise.all([
|
||||||
|
fetchDashboardPersons(),
|
||||||
|
fetchDashboardDueDates(),
|
||||||
|
]);
|
||||||
|
dashboardPersons.value = persons;
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -509,6 +530,7 @@ export function useDashboardPersons() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dashboardPersons,
|
dashboardPersons,
|
||||||
|
dashboardDueDates,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||||
|
import log from "loglevel";
|
||||||
|
import { useCourseData, useDashboardPersons } from "@/composables";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import _ from "lodash";
|
||||||
|
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
||||||
|
|
||||||
|
log.debug("DashboardPersonsPage created");
|
||||||
|
|
||||||
|
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
|
||||||
|
|
||||||
|
type DropboxItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CourseItem = DropboxItem & {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { loading, dashboardDueDates } = useDashboardPersons();
|
||||||
|
|
||||||
|
const courses = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: UNFILTERED,
|
||||||
|
name: t("a.Alle Lehrgänge"),
|
||||||
|
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("a.AlleDurchführungen"),
|
||||||
|
},
|
||||||
|
...sessions,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSession = ref<DropboxItem>(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 initialItemCircle: DropboxItem = {
|
||||||
|
id: UNFILTERED,
|
||||||
|
name: t("a.AlleCircle"),
|
||||||
|
};
|
||||||
|
const circles = ref<DropboxItem[]>([initialItemCircle]);
|
||||||
|
const selectedCircle = ref<DropboxItem>(circles.value[0]);
|
||||||
|
|
||||||
|
async function loadCircleValues() {
|
||||||
|
if (selectedCourse.value && selectedCourse.value.slug) {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
await loadCircleValues();
|
||||||
|
});
|
||||||
|
</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">
|
||||||
|
<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="session-select"
|
||||||
|
:items="courses"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-if="courseSessions.length > 2"
|
||||||
|
v-model="selectedSession"
|
||||||
|
data-cy="session-select"
|
||||||
|
:items="courseSessions"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-if="circles.length > 2"
|
||||||
|
v-model="selectedCircle"
|
||||||
|
data-cy="appointments-circle-select"
|
||||||
|
:items="circles"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-if="dueDateTypes.length > 2"
|
||||||
|
v-model="selectedType"
|
||||||
|
data-cy="appointments-type-select"
|
||||||
|
:items="dueDateTypes"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<DueDatesList
|
||||||
|
:show-top-border="false"
|
||||||
|
:show-bottom-border="false"
|
||||||
|
:due-dates="filteredDueDates"
|
||||||
|
:show-all-due-dates-link="false"
|
||||||
|
:max-count="100"
|
||||||
|
data-cy="due-date-list"
|
||||||
|
:show-course-session="true"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -19,7 +19,7 @@ type MenuItem = {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { loading: loadingPersons, dashboardPersons } = useDashboardPersons();
|
const { loading, dashboardPersons } = useDashboardPersons();
|
||||||
|
|
||||||
const courses = computed(() => {
|
const courses = computed(() => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -64,10 +64,6 @@ const courseSessions = computed(() => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(selectedCourse, () => {
|
|
||||||
selectedSession.value = courseSessions.value[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedSession = ref<MenuItem>(courseSessions.value[0]);
|
const selectedSession = ref<MenuItem>(courseSessions.value[0]);
|
||||||
|
|
||||||
const filteredPersons = computed(() => {
|
const filteredPersons = computed(() => {
|
||||||
|
|
@ -113,11 +109,15 @@ function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessio
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(selectedCourse, () => {
|
||||||
|
selectedSession.value = courseSessions.value[0];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="loadingPersons" class="m-8 flex justify-center">
|
<div v-if="loading" class="m-8 flex justify-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bg-gray-200">
|
<div v-else class="bg-gray-200">
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ const router = createRouter({
|
||||||
path: "/dashboard/persons",
|
path: "/dashboard/persons",
|
||||||
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
|
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/duedates",
|
||||||
|
component: () => import("@/pages/dashboard/DashboardDueDatesPage.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/course/:courseSlug/media",
|
path: "/course/:courseSlug/media",
|
||||||
props: true,
|
props: true,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
CourseStatisticsType,
|
CourseStatisticsType,
|
||||||
DashboardConfigType,
|
DashboardConfigType,
|
||||||
} from "@/gql/graphql";
|
} from "@/gql/graphql";
|
||||||
|
import type { DueDate } from "@/types";
|
||||||
|
|
||||||
export type DashboardPersonRoleType =
|
export type DashboardPersonRoleType =
|
||||||
| "SUPERVISOR"
|
| "SUPERVISOR"
|
||||||
|
|
@ -70,6 +71,11 @@ export type DashboardCourseConfigType = {
|
||||||
session_to_continue_id: string;
|
session_to_continue_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DashboardDueDate = DueDate & {
|
||||||
|
course_session: DashboardPersonCourseSessionType;
|
||||||
|
translatedType: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchStatisticData = async (
|
export const fetchStatisticData = async (
|
||||||
courseId: string
|
courseId: string
|
||||||
): Promise<CourseStatisticsType | null> => {
|
): Promise<CourseStatisticsType | null> => {
|
||||||
|
|
@ -144,6 +150,10 @@ export async function fetchDashboardPersons() {
|
||||||
return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/");
|
return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchDashboardDueDates() {
|
||||||
|
return await itGetCached<DashboardDueDate[]>("/api/dashboard/duedates/");
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchDashboardConfigv2() {
|
export async function fetchDashboardConfigv2() {
|
||||||
return await itGetCached<DashboardCourseConfigType[]>("/api/dashboard/config/");
|
return await itGetCached<DashboardCourseConfigType[]>("/api/dashboard/config/");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ def get_dashboard_due_dates(request):
|
||||||
"course_id": str(cs.course.id),
|
"course_id": str(cs.course.id),
|
||||||
"course_title": cs.course.title,
|
"course_title": cs.course.title,
|
||||||
"course_slug": cs.course.slug,
|
"course_slug": cs.course.slug,
|
||||||
"user_role": user_role(cs.roles),
|
"my_role": user_role(cs.roles),
|
||||||
"is_uk": cs.course.configuration.is_uk,
|
"is_uk": cs.course.configuration.is_uk,
|
||||||
"is_vv": cs.course.configuration.is_vv,
|
"is_vv": cs.course.configuration.is_vv,
|
||||||
}
|
}
|
||||||
|
|
@ -445,7 +445,9 @@ def get_mentor_open_tasks_count(request, course_id: str):
|
||||||
return Response(
|
return Response(
|
||||||
status=200,
|
status=200,
|
||||||
data={
|
data={
|
||||||
"open_task_count": _get_mentor_open_tasks_count(course_id, request.user) # noqa
|
"open_task_count": _get_mentor_open_tasks_count(
|
||||||
|
course_id, request.user
|
||||||
|
) # noqa
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except PermissionDenied as e:
|
except PermissionDenied as e:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue