Add new DashboardDueDatesPage

This commit is contained in:
Daniel Egger 2024-04-18 18:48:53 +02:00
parent 50c35b7100
commit 102196a290
7 changed files with 298 additions and 27 deletions

View File

@ -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">

View File

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

View File

@ -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>

View File

@ -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">

View File

@ -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,

View File

@ -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/");
} }

View File

@ -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: