Add frontend export
This commit is contained in:
parent
5b60e50ac4
commit
033886f00b
|
|
@ -3,21 +3,17 @@ import { computed, ref, watch } from "vue";
|
|||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
|
||||
import type { StatisticsFilterItem } from "@/types";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
interface Item {
|
||||
_id: string;
|
||||
course_session_id: string;
|
||||
generation: string;
|
||||
circle_id: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
items: Item[];
|
||||
items: StatisticsFilterItem[];
|
||||
courseSessionProperties: StatisticsCourseSessionPropertiesType;
|
||||
}>();
|
||||
|
||||
defineExpose({ getFilteredItems });
|
||||
|
||||
const sessionFilter = computed(() => {
|
||||
const f = props.courseSessionProperties.sessions.map((session) => ({
|
||||
name: `${t("a.Durchfuehrung")}: ${session.name}`,
|
||||
|
|
@ -71,6 +67,10 @@ const filteredItems = computed(() => {
|
|||
return sessionMatch && generationMatch && circleMatch;
|
||||
});
|
||||
});
|
||||
|
||||
function getFilteredItems() {
|
||||
return filteredItems.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
|
|||
import { getDateString } from "@/components/dueDates/dueDatesUtils";
|
||||
import dayjs from "dayjs";
|
||||
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
|
||||
const props = defineProps<{
|
||||
|
|
@ -17,6 +20,8 @@ const props = defineProps<{
|
|||
circleMeta: (circleId: string) => StatisticsCircleDataType;
|
||||
}>();
|
||||
|
||||
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
|
||||
|
||||
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
|
||||
if (!metrics.ranking_completed) {
|
||||
return {
|
||||
|
|
@ -36,15 +41,28 @@ const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
|
|||
const total = (metrics: AssignmentCompletionMetricsType) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<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 v-if="courseStatistics?.assignments.records" class="mt-8 bg-white">
|
||||
<StatisticFilterList
|
||||
ref="statisticFilter"
|
||||
:course-session-properties="courseStatistics?.course_session_properties"
|
||||
:items="courseStatistics.assignments.records"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
|
|||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import { getDateString } from "@/components/dueDates/dueDatesUtils";
|
||||
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
|
||||
const props = defineProps<{
|
||||
|
|
@ -16,6 +19,8 @@ const props = defineProps<{
|
|||
circleMeta: (circleId: string) => StatisticsCircleDataType;
|
||||
}>();
|
||||
|
||||
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
|
||||
|
||||
const attendanceStats = (present: number, total: number) => {
|
||||
return {
|
||||
SUCCESS: present,
|
||||
|
|
@ -23,18 +28,31 @@ const attendanceStats = (present: number, total: number) => {
|
|||
UNKNOWN: 0,
|
||||
};
|
||||
};
|
||||
|
||||
async function exportData() {
|
||||
if (!statisticFilter.value) {
|
||||
return;
|
||||
}
|
||||
const filteredItems = statisticFilter.value.getFilteredItems();
|
||||
await exportDataAsXls(filteredItems, exportAttendance);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<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
|
||||
v-if="courseStatistics?.attendance_day_presences.records"
|
||||
class="mt-8 bg-white"
|
||||
>
|
||||
<StatisticFilterList
|
||||
ref="statisticFilter"
|
||||
:course-session-properties="courseStatistics.course_session_properties"
|
||||
:items="courseStatistics.attendance_day_presences.records"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import type {
|
|||
} from "@/gql/graphql";
|
||||
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
|
||||
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
|
||||
const props = defineProps<{
|
||||
|
|
@ -14,15 +17,30 @@ const props = defineProps<{
|
|||
courseSessionName: (sessionId: string) => string;
|
||||
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>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<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 v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white">
|
||||
<StatisticFilterList
|
||||
ref="statisticFilter"
|
||||
:course-session-properties="courseStatistics.course_session_properties"
|
||||
:items="courseStatistics.feedback_responses.records"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ import type {
|
|||
CourseStatisticsType,
|
||||
DashboardConfigType,
|
||||
} from "@/gql/graphql";
|
||||
import type { DashboardPersonsPageMode, DueDate } from "@/types";
|
||||
import type {
|
||||
DashboardPersonsPageMode,
|
||||
DueDate,
|
||||
XlsExportRequestData,
|
||||
XlsExportResponseData,
|
||||
} from "@/types";
|
||||
|
||||
export type DashboardPersonRoleType =
|
||||
| "SUPERVISOR"
|
||||
|
|
@ -189,25 +194,22 @@ export async function fetchOpenTasksCount(courseId: string) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function exportFeedback(data: {
|
||||
courseSessionIds: string[];
|
||||
circleIds: string[];
|
||||
}) {
|
||||
export async function exportFeedback(
|
||||
data: XlsExportRequestData
|
||||
): Promise<XlsExportResponseData> {
|
||||
return await itPost("/api/dashboard/export/feedback/", data);
|
||||
}
|
||||
|
||||
export async function exportAttendance(data: {
|
||||
courseSessionIds: string[];
|
||||
circleIds: string[];
|
||||
}) {
|
||||
export async function exportAttendance(
|
||||
data: XlsExportRequestData
|
||||
): Promise<XlsExportResponseData> {
|
||||
return await itPost("/api/dashboard/export/attendance/", data);
|
||||
}
|
||||
|
||||
export async function exportCertificate(data: {
|
||||
courseSessionIds: string[];
|
||||
circleIds: string[];
|
||||
}) {
|
||||
return await itPost("/api/dashboard/export/certificate/", data);
|
||||
export async function exportCompetenceElements(
|
||||
data: XlsExportRequestData
|
||||
): Promise<XlsExportResponseData> {
|
||||
return await itPost("/api/dashboard/export/competence_elements/", data);
|
||||
}
|
||||
|
||||
export function courseIdForCourseSlug(
|
||||
|
|
|
|||
|
|
@ -618,3 +618,20 @@ export type User = {
|
|||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,58 @@
|
|||
/**
|
||||
* Created by christiancueni on 17.06.2024.
|
||||
* Copyright (c) 2024 ITerativ GmbH. All rights reserved.
|
||||
*/
|
||||
import type {
|
||||
StatisticsFilterItem,
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ urlpatterns = [
|
|||
path(r"api/dashboard/course/<str:course_id>/open_tasks/", 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/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"),
|
||||
|
||||
# course
|
||||
|
|
|
|||
Loading…
Reference in New Issue