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 { useCourseSessionsStore } from "@/stores/courseSessions";
import { useTranslation } from "i18next-vue";
import dayjs from "dayjs";
import { computed } from "vue";
import dayjs from "dayjs";
const props = defineProps<{
dueDate: DueDate;
@ -48,24 +48,23 @@ const courseSessionTitle = computed(() => {
<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>
{{ " " }}
{{ 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>
</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>
<span v-if="props.showCourseSession ?? courseSessionTitle">

View File

@ -8,8 +8,12 @@ import {
circleFlatLearningUnits,
someFinishedInLearningSequence,
} from "@/services/circle";
import type { DashboardPersonType } from "@/services/dashboard";
import { fetchDashboardPersons, fetchStatisticData } from "@/services/dashboard";
import type { DashboardDueDate, DashboardPersonType } from "@/services/dashboard";
import {
fetchDashboardDueDates,
fetchDashboardPersons,
fetchStatisticData,
} from "@/services/dashboard";
import { presignUpload, uploadFile } from "@/services/files";
import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions";
@ -30,6 +34,7 @@ import type {
PerformanceCriteria,
} from "@/types";
import { useQuery } from "@urql/vue";
import { t } from "i18next";
import orderBy from "lodash/orderBy";
import log from "loglevel";
import type { ComputedRef, Ref } from "vue";
@ -494,12 +499,28 @@ export function useMyLearningMentors() {
export function useDashboardPersons() {
const dashboardPersons = ref<DashboardPersonType[]>([]);
const dashboardDueDates = ref<DashboardDueDate[]>([]);
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
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 {
loading.value = false;
}
@ -509,6 +530,7 @@ export function useDashboardPersons() {
return {
dashboardPersons,
dashboardDueDates,
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 { loading: loadingPersons, dashboardPersons } = useDashboardPersons();
const { loading, dashboardPersons } = useDashboardPersons();
const courses = computed(() => {
return [
@ -64,10 +64,6 @@ const courseSessions = computed(() => {
];
});
watch(selectedCourse, () => {
selectedSession.value = courseSessions.value[0];
});
const selectedSession = ref<MenuItem>(courseSessions.value[0]);
const filteredPersons = computed(() => {
@ -113,11 +109,15 @@ function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessio
return "";
}
watch(selectedCourse, () => {
selectedSession.value = courseSessions.value[0];
});
</script>
<template>
<div>
<div v-if="loadingPersons" class="m-8 flex justify-center">
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="bg-gray-200">

View File

@ -65,6 +65,10 @@ const router = createRouter({
path: "/dashboard/persons",
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
},
{
path: "/dashboard/duedates",
component: () => import("@/pages/dashboard/DashboardDueDatesPage.vue"),
},
{
path: "/course/:courseSlug/media",
props: true,

View File

@ -13,6 +13,7 @@ import type {
CourseStatisticsType,
DashboardConfigType,
} from "@/gql/graphql";
import type { DueDate } from "@/types";
export type DashboardPersonRoleType =
| "SUPERVISOR"
@ -70,6 +71,11 @@ export type DashboardCourseConfigType = {
session_to_continue_id: string;
};
export type DashboardDueDate = DueDate & {
course_session: DashboardPersonCourseSessionType;
translatedType: string;
};
export const fetchStatisticData = async (
courseId: string
): Promise<CourseStatisticsType | null> => {
@ -144,6 +150,10 @@ export async function fetchDashboardPersons() {
return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/");
}
export async function fetchDashboardDueDates() {
return await itGetCached<DashboardDueDate[]>("/api/dashboard/duedates/");
}
export async function fetchDashboardConfigv2() {
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_title": cs.course.title,
"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_vv": cs.course.configuration.is_vv,
}
@ -445,7 +445,9 @@ def get_mentor_open_tasks_count(request, course_id: str):
return Response(
status=200,
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: