Add frontend export

This commit is contained in:
Christian Cueni 2024-06-17 16:32:27 +02:00
parent 5b60e50ac4
commit 033886f00b
8 changed files with 155 additions and 27 deletions

View File

@ -3,21 +3,17 @@ import { computed, ref, watch } from "vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql"; import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
import type { StatisticsFilterItem } from "@/types";
const { t } = useTranslation(); const { t } = useTranslation();
interface Item {
_id: string;
course_session_id: string;
generation: string;
circle_id: string;
}
const props = defineProps<{ const props = defineProps<{
items: Item[]; items: StatisticsFilterItem[];
courseSessionProperties: StatisticsCourseSessionPropertiesType; courseSessionProperties: StatisticsCourseSessionPropertiesType;
}>(); }>();
defineExpose({ getFilteredItems });
const sessionFilter = computed(() => { const sessionFilter = computed(() => {
const f = props.courseSessionProperties.sessions.map((session) => ({ const f = props.courseSessionProperties.sessions.map((session) => ({
name: `${t("a.Durchfuehrung")}: ${session.name}`, name: `${t("a.Durchfuehrung")}: ${session.name}`,
@ -71,6 +67,10 @@ const filteredItems = computed(() => {
return sessionMatch && generationMatch && circleMatch; return sessionMatch && generationMatch && circleMatch;
}); });
}); });
function getFilteredItems() {
return filteredItems.value;
}
</script> </script>
<template> <template>

View File

@ -9,6 +9,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
import { getDateString } from "@/components/dueDates/dueDatesUtils"; import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { type Ref, ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportCompetenceElements } from "@/services/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ const props = defineProps<{
@ -17,6 +20,8 @@ const props = defineProps<{
circleMeta: (circleId: string) => StatisticsCircleDataType; circleMeta: (circleId: string) => StatisticsCircleDataType;
}>(); }>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => { const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
if (!metrics.ranking_completed) { if (!metrics.ranking_completed) {
return { return {
@ -36,15 +41,28 @@ const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
const total = (metrics: AssignmentCompletionMetricsType) => { const total = (metrics: AssignmentCompletionMetricsType) => {
return metrics.passed_count + metrics.failed_count + metrics.unranked_count; return metrics.passed_count + metrics.failed_count + metrics.unranked_count;
}; };
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportCompetenceElements);
}
</script> </script>
<template> <template>
<main> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3> <h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3>
<button class="flex" @click="exportData">
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div> </div>
<div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white"> <div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white">
<StatisticFilterList <StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics?.course_session_properties" :course-session-properties="courseStatistics?.course_session_properties"
:items="courseStatistics.assignments.records" :items="courseStatistics.assignments.records"
> >

View File

@ -8,6 +8,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { getDateString } from "@/components/dueDates/dueDatesUtils"; import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ref, type Ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportAttendance } from "@/services/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ const props = defineProps<{
@ -16,6 +19,8 @@ const props = defineProps<{
circleMeta: (circleId: string) => StatisticsCircleDataType; circleMeta: (circleId: string) => StatisticsCircleDataType;
}>(); }>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const attendanceStats = (present: number, total: number) => { const attendanceStats = (present: number, total: number) => {
return { return {
SUCCESS: present, SUCCESS: present,
@ -23,18 +28,31 @@ const attendanceStats = (present: number, total: number) => {
UNKNOWN: 0, UNKNOWN: 0,
}; };
}; };
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportAttendance);
}
</script> </script>
<template> <template>
<main> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("Anwesenheit") }}</h3> <h3>{{ $t("Anwesenheit") }}</h3>
<button class="flex" @click="exportData">
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div> </div>
<div <div
v-if="courseStatistics?.attendance_day_presences.records" v-if="courseStatistics?.attendance_day_presences.records"
class="mt-8 bg-white" class="mt-8 bg-white"
> >
<StatisticFilterList <StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics.course_session_properties" :course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.attendance_day_presences.records" :items="courseStatistics.attendance_day_presences.records"
> >

View File

@ -7,6 +7,9 @@ import type {
} from "@/gql/graphql"; } from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { getBlendedColorForRating } from "@/utils/ratingToColor"; import { getBlendedColorForRating } from "@/utils/ratingToColor";
import { ref, type Ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportFeedback } from "@/services/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ const props = defineProps<{
@ -14,15 +17,30 @@ const props = defineProps<{
courseSessionName: (sessionId: string) => string; courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType; circleMeta: (circleId: string) => StatisticsCircleDataType;
}>(); }>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportFeedback);
}
</script> </script>
<template> <template>
<main> <main>
<div class="mb-10 flex items-center justify-between"> <div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Feedback Teilnehmer") }}</h3> <h3>{{ $t("a.Feedback Teilnehmer") }}</h3>
<button class="flex" @click="exportData">
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div> </div>
<div v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white"> <div v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white">
<StatisticFilterList <StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics.course_session_properties" :course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.feedback_responses.records" :items="courseStatistics.feedback_responses.records"
> >

View File

@ -13,7 +13,12 @@ import type {
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import type { DashboardPersonsPageMode, DueDate } from "@/types"; import type {
DashboardPersonsPageMode,
DueDate,
XlsExportRequestData,
XlsExportResponseData,
} from "@/types";
export type DashboardPersonRoleType = export type DashboardPersonRoleType =
| "SUPERVISOR" | "SUPERVISOR"
@ -189,25 +194,22 @@ export async function fetchOpenTasksCount(courseId: string) {
); );
} }
export async function exportFeedback(data: { export async function exportFeedback(
courseSessionIds: string[]; data: XlsExportRequestData
circleIds: string[]; ): Promise<XlsExportResponseData> {
}) {
return await itPost("/api/dashboard/export/feedback/", data); return await itPost("/api/dashboard/export/feedback/", data);
} }
export async function exportAttendance(data: { export async function exportAttendance(
courseSessionIds: string[]; data: XlsExportRequestData
circleIds: string[]; ): Promise<XlsExportResponseData> {
}) {
return await itPost("/api/dashboard/export/attendance/", data); return await itPost("/api/dashboard/export/attendance/", data);
} }
export async function exportCertificate(data: { export async function exportCompetenceElements(
courseSessionIds: string[]; data: XlsExportRequestData
circleIds: string[]; ): Promise<XlsExportResponseData> {
}) { return await itPost("/api/dashboard/export/competence_elements/", data);
return await itPost("/api/dashboard/export/certificate/", data);
} }
export function courseIdForCourseSlug( export function courseIdForCourseSlug(

View File

@ -618,3 +618,20 @@ export type User = {
}; };
export type DashboardPersonsPageMode = "default" | "competenceMetrics"; export type DashboardPersonsPageMode = "default" | "competenceMetrics";
export interface StatisticsFilterItem {
_id: string;
course_session_id: string;
generation: string;
circle_id: string;
}
export interface XlsExportRequestData {
courseSessionIds: number[];
circleIds: number[];
}
export interface XlsExportResponseData {
encoded_data: string;
file_name: string;
}

View File

@ -1,4 +1,58 @@
/** import type {
* Created by christiancueni on 17.06.2024. StatisticsFilterItem,
* Copyright (c) 2024 ITerativ GmbH. All rights reserved. XlsExportRequestData,
*/ XlsExportResponseData,
} from "@/types";
interface exportApiCall {
(data: XlsExportRequestData): Promise<XlsExportResponseData>;
}
export async function exportDataAsXls(
items: StatisticsFilterItem[],
apiCall: exportApiCall
) {
const itemIds = extractUniqueIds(items);
const data = await apiCall(itemIds);
openDataAsXls(data.encoded_data, data.file_name);
}
export async function openDataAsXls(encodedData: string, filename: string) {
// Decode base64 string to binary data
const byteCharacters = atob(encodedData);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: "application/octet-stream" });
// Create a link element and trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
// Function to extract unique circle_ids and course_session_ids
export function extractUniqueIds(data: StatisticsFilterItem[]): {
circleIds: number[];
courseSessionIds: number[];
} {
const circleIdsSet = new Set<number>();
const courseSessionIdsSet = new Set<number>();
data.forEach((item) => {
circleIdsSet.add(Number(item.circle_id));
courseSessionIdsSet.add(Number(item.course_session_id));
});
return {
circleIds: Array.from(circleIdsSet),
courseSessionIds: Array.from(courseSessionIdsSet),
};
}

View File

@ -135,7 +135,8 @@ urlpatterns = [
path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count, path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count,
name="get_mentor_open_tasks_count"), name="get_mentor_open_tasks_count"),
path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"), path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"),
path(r"api/dashboard/export/certificate/", export_competence_elements_as_xsl, name="export_certificate_as_xsl"), path(r"api/dashboard/export/competence_elements/", export_competence_elements_as_xsl,
name="export_certificate_as_xsl"),
path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"), path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"),
# course # course