541 lines
16 KiB
Vue
541 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import { useChosenProfileMapping } from "@/components/dashboard/composables";
|
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
|
import { useDashboardPersonsDueDates } from "@/composables";
|
|
import {
|
|
type DashboardPersonCourseSessionType,
|
|
exportPersons,
|
|
} from "@/services/dashboard";
|
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
|
import { useUserStore } from "@/stores/user";
|
|
import type { DashboardPersonsPageMode } from "@/types";
|
|
import { openDataAsXls } from "@/utils/export";
|
|
import { useRouteQuery } from "@vueuse/router";
|
|
import dayjs from "dayjs";
|
|
import { useTranslation } from "i18next-vue";
|
|
import _ from "lodash";
|
|
import log from "loglevel";
|
|
import { computed, ref, watch } from "vue";
|
|
|
|
log.debug("DashboardPersonsPage created");
|
|
|
|
export interface Props {
|
|
mode?: DashboardPersonsPageMode;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
mode: "default",
|
|
});
|
|
|
|
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
|
|
|
|
type MenuItem = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
const { t } = useTranslation();
|
|
const userStore = useUserStore();
|
|
|
|
const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode);
|
|
const { CHOSEN_PROFILE_TO_NAME } = useChosenProfileMapping();
|
|
const { allCourseSessionActions } = useCourseSessionsStore();
|
|
|
|
const courses = computed(() => {
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("Lehrgang")}: ${t("a.Alle")}`,
|
|
},
|
|
..._(dashboardPersons.value)
|
|
.flatMap((person) => person.course_sessions)
|
|
.map((cs) => {
|
|
return { name: cs.course_title, id: cs.course_id };
|
|
})
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value(),
|
|
];
|
|
});
|
|
const selectedCourse = ref<MenuItem>(courses.value[0]);
|
|
const selectedCourseRouteQuery = useRouteQuery("course", UNFILTERED, {
|
|
mode: "replace",
|
|
});
|
|
|
|
watch(selectedCourse, () => {
|
|
selectedCourseRouteQuery.value = selectedCourse.value.id;
|
|
});
|
|
watch(courses, () => {
|
|
if (selectedCourseRouteQuery.value !== UNFILTERED) {
|
|
selectedCourse.value =
|
|
courses.value.find((course) => course.id === selectedCourseRouteQuery.value) ||
|
|
courses.value[0];
|
|
}
|
|
});
|
|
|
|
const regions = computed(() => {
|
|
let values = _(dashboardPersons.value)
|
|
.flatMap((person) => person.course_sessions)
|
|
.map((cs) => {
|
|
return Object.assign({}, cs, { name: cs.region, id: cs.region });
|
|
})
|
|
.filter((cs) => !!cs.region)
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
// filter by selected course
|
|
if (selectedCourse.value.id !== UNFILTERED) {
|
|
values = values.filter((cs) => cs.course_id === selectedCourse.value.id);
|
|
}
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("a.Region")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedRegion = ref<MenuItem>(regions.value[0]);
|
|
|
|
const courseSessions = computed(() => {
|
|
let values = _(dashboardPersons.value)
|
|
.flatMap((person) => person.course_sessions)
|
|
.map((cs) => {
|
|
return Object.assign({}, cs, { name: cs.session_title, id: cs.id });
|
|
})
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
// filter by selected course
|
|
if (selectedCourse.value.id !== UNFILTERED) {
|
|
values = values.filter((cs) => cs.course_id === selectedCourse.value.id);
|
|
}
|
|
// filter by selected region
|
|
if (selectedRegion.value.id !== UNFILTERED) {
|
|
values = values.filter((cs) => cs.region === selectedRegion.value.id);
|
|
}
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("Durchführung")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedSession = ref<MenuItem>(courseSessions.value[0]);
|
|
|
|
const generations = computed(() => {
|
|
const values = _(dashboardPersons.value)
|
|
.flatMap((person) => person.course_sessions)
|
|
.map((cs) => {
|
|
return Object.assign({}, cs, { name: cs.generation, id: cs.generation });
|
|
})
|
|
.filter((cs) => !!cs.generation)
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("a.Generation")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedGeneration = ref<MenuItem>(generations.value[0]);
|
|
|
|
const roles = computed(() => {
|
|
const values = _(dashboardPersons.value)
|
|
.flatMap((person) => person.course_sessions)
|
|
.map((cs) => {
|
|
return Object.assign({}, cs, {
|
|
name: cs.user_role_display,
|
|
id: cs.user_role_display,
|
|
});
|
|
})
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("a.Rolle")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedRole = ref<MenuItem>(roles.value[0]);
|
|
|
|
const chosenProfiles = computed(() => {
|
|
const values = _(dashboardPersons.value)
|
|
.map((cs) => {
|
|
return Object.assign({}, cs, {
|
|
name: CHOSEN_PROFILE_TO_NAME[cs.chosen_profile],
|
|
id: cs.chosen_profile,
|
|
});
|
|
})
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("a.Zulassungsprofil")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedChosenProfile = ref<MenuItem>(chosenProfiles.value[0]);
|
|
|
|
const paidYears = computed(() => {
|
|
const values = _(dashboardPersons.value)
|
|
.filter((cs) => dayjs(cs.paid_datetime).isValid())
|
|
.map((cs) => {
|
|
const paidYear = dayjs(cs.paid_datetime).format("YYYY");
|
|
return Object.assign({}, cs, {
|
|
name: paidYear,
|
|
id: paidYear,
|
|
});
|
|
})
|
|
.uniqBy("id")
|
|
.orderBy("name")
|
|
.value();
|
|
|
|
return [
|
|
{
|
|
id: UNFILTERED,
|
|
name: `${t("a.Jahr")}: ${t("a.Alle")}`,
|
|
},
|
|
...values,
|
|
];
|
|
});
|
|
const selectedPaidYear = ref<MenuItem>(paidYears.value[0]);
|
|
const selectedPaidYearQuery = useRouteQuery("selectedPaidYear", UNFILTERED, {
|
|
mode: "replace",
|
|
});
|
|
watch(selectedPaidYear, () => {
|
|
selectedPaidYearQuery.value = selectedPaidYear.value.id;
|
|
});
|
|
watch(paidYears, () => {
|
|
if (selectedPaidYearQuery.value !== UNFILTERED) {
|
|
selectedPaidYear.value =
|
|
paidYears.value.find((paidYear) => paidYear.id === selectedPaidYearQuery.value) ||
|
|
paidYears.value[0];
|
|
}
|
|
});
|
|
|
|
const filteredPersons = computed(() => {
|
|
return _.orderBy(
|
|
dashboardPersons.value
|
|
.filter((person) => {
|
|
if (selectedCourse.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.course_sessions.some(
|
|
(cs) => cs.course_id === selectedCourse.value.id
|
|
);
|
|
})
|
|
.filter((person) => {
|
|
if (selectedSession.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.course_sessions.some((cs) => cs.id === selectedSession.value.id);
|
|
})
|
|
.filter((person) => {
|
|
if (selectedRegion.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.course_sessions.some(
|
|
(cs) => cs.region === selectedRegion.value.id
|
|
);
|
|
})
|
|
.filter((person) => {
|
|
if (selectedGeneration.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.course_sessions.some(
|
|
(cs) => cs.generation === selectedGeneration.value.id
|
|
);
|
|
})
|
|
.filter((person) => {
|
|
if (selectedRole.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.course_sessions.some(
|
|
(cs) => cs.user_role_display === selectedRole.value.id
|
|
);
|
|
})
|
|
.filter((person) => {
|
|
if (selectedChosenProfile.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
return person.chosen_profile === selectedChosenProfile.value.id;
|
|
})
|
|
.filter((person) => {
|
|
if (selectedPaidYear.value.id === UNFILTERED) {
|
|
return true;
|
|
}
|
|
const paidYear = dayjs(person.paid_datetime).format("YYYY");
|
|
return paidYear == selectedPaidYear.value.id;
|
|
}),
|
|
["last_name", "first_name"]
|
|
);
|
|
});
|
|
|
|
const filtersVisible = computed(() => {
|
|
return (
|
|
courses.value.length > 2 ||
|
|
courseSessions.value.length > 2 ||
|
|
regions.value.length > 2 ||
|
|
generations.value.length > 2 ||
|
|
roles.value.length > 2 ||
|
|
chosenProfiles.value.length > 2 ||
|
|
paidYears.value.length > 2
|
|
);
|
|
});
|
|
|
|
const canExport = computed(() => {
|
|
// If user is only member, he can't export
|
|
// member = has is_member property, but not is_expert or is_supervisor
|
|
if (!allCourseSessionActions.has("is_member")) {
|
|
// is berufsbildner or expert or supervisor
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
allCourseSessionActions.has("is_expert") ||
|
|
allCourseSessionActions.has("is_supervisor")
|
|
) {
|
|
// is member
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessionType) {
|
|
if (
|
|
[
|
|
"SUPERVISOR",
|
|
"EXPERT",
|
|
"LEARNING_MENTOR",
|
|
"BERUFSBILDNER",
|
|
"LEARNING_MENTOR",
|
|
].includes(personCourseSession.user_role)
|
|
) {
|
|
return personCourseSession.user_role_display;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
async function exportData() {
|
|
const requestData = {
|
|
courseSessionUserIds: filteredPersons.value.map((person) => person.csu_id),
|
|
};
|
|
const data = await exportPersons(requestData, userStore.language);
|
|
openDataAsXls(data.encoded_data, data.file_name);
|
|
}
|
|
|
|
watch(selectedCourse, () => {
|
|
selectedRegion.value = regions.value[0];
|
|
});
|
|
|
|
watch(selectedRegion, () => {
|
|
selectedSession.value = courseSessions.value[0];
|
|
selectedGeneration.value = generations.value[0];
|
|
selectedRole.value = roles.value[0];
|
|
});
|
|
</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">
|
|
<router-link
|
|
:to="`/`"
|
|
class="btn-text inline-flex items-center p-0"
|
|
data-cy="back-to-learning-path-button"
|
|
>
|
|
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
|
|
<span class="inline">{{ $t("general.back") }}</span>
|
|
</router-link>
|
|
<div class="mb-10 flex items-center justify-between">
|
|
<h2 class="my-4">{{ $t("a.Personen") }}</h2>
|
|
<button
|
|
v-if="canExport"
|
|
class="flex"
|
|
data-cy="export-button"
|
|
@click="exportData"
|
|
>
|
|
<it-icon-export></it-icon-export>
|
|
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
|
|
</button>
|
|
</div>
|
|
<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="select-course"
|
|
:items="courses"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="regions.length > 2"
|
|
v-model="selectedRegion"
|
|
data-cy="select-region"
|
|
:items="regions"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="courseSessions.length > 2"
|
|
v-model="selectedSession"
|
|
data-cy="select-course"
|
|
:items="courseSessions"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="generations.length > 2"
|
|
v-model="selectedGeneration"
|
|
data-cy="select-generation"
|
|
:items="generations"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="roles.length > 2"
|
|
v-model="selectedRole"
|
|
data-cy="select-role"
|
|
:items="roles"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="chosenProfiles.length > 2"
|
|
v-model="selectedChosenProfile"
|
|
data-cy="select-chosen-profile"
|
|
:items="chosenProfiles"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
|
|
<ItDropdownSelect
|
|
v-if="paidYears.length > 2"
|
|
v-model="selectedPaidYear"
|
|
data-cy="select-paid-year"
|
|
:items="paidYears"
|
|
borderless
|
|
></ItDropdownSelect>
|
|
</section>
|
|
<div
|
|
v-for="person in filteredPersons"
|
|
:key="person.user_id"
|
|
data-cy="person"
|
|
class="flex flex-col justify-between gap-4 border-b p-2 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
|
|
>
|
|
<div class="w-full flex-auto md:w-1/3">
|
|
<div class="flex items-center space-x-2">
|
|
<img
|
|
class="inline-block h-11 w-11 rounded-full"
|
|
:src="
|
|
person.avatar_url_small ||
|
|
'/static/avatars/myvbv-default-avatar.png'
|
|
"
|
|
:alt="`${person.first_name} ${person.last_name}`"
|
|
/>
|
|
<div>
|
|
<div class="text-bold">
|
|
{{ person.first_name }}
|
|
{{ person.last_name }}
|
|
</div>
|
|
<div class="text-gray-900">{{ person.email }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full flex-auto items-start md:w-2/3">
|
|
<div
|
|
v-for="cs in person.course_sessions"
|
|
:key="cs.id"
|
|
class="w-full border-b pb-2 pt-2 first:pt-0 last:border-b-0 last:pb-0"
|
|
>
|
|
<div class="flex flex-col md:flex-row md:items-center">
|
|
<div v-if="props.mode === 'default'" class="md:w-1/2">
|
|
<div class="text-gray-900">{{ cs.course_title }}</div>
|
|
<div v-if="cs.is_uk">{{ cs.session_title }}</div>
|
|
</div>
|
|
<div v-if="props.mode === 'competenceMetrics'" class="md:w-1/3">
|
|
<div
|
|
v-if="cs.competence_metrics?.passed_count || 0 > 0"
|
|
class="my-2 w-fit rounded-md bg-green-200 px-2.5 py-0.5"
|
|
>
|
|
{{ $t("a.Bestanden") }}:
|
|
{{ cs.competence_metrics?.passed_count }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="props.mode === 'default'" class="md:w-1/4">
|
|
{{ personRoleDisplayValue(cs) }}
|
|
</div>
|
|
<div v-else-if="props.mode === 'competenceMetrics'" class="md:w-1/3">
|
|
<div
|
|
v-if="cs.competence_metrics?.failed_count || 0 > 0"
|
|
class="my-2 w-fit rounded-md bg-error-red-200 px-2.5 py-0.5"
|
|
>
|
|
{{ $t("a.Nicht bestanden") }}:
|
|
{{ cs.competence_metrics?.failed_count }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="md:w-1/4 md:text-right">
|
|
<div
|
|
v-if="
|
|
(['SUPERVISOR', 'EXPERT'].includes(cs.my_role) &&
|
|
cs.user_role === 'MEMBER') ||
|
|
(['LEARNING_MENTOR', 'BERUFSBILDNER'].includes(cs.my_role) &&
|
|
cs.user_role === 'PARTICIPANT')
|
|
"
|
|
>
|
|
<router-link
|
|
:to="{
|
|
name: 'profileLearningPath',
|
|
params: {
|
|
userId: person.user_id,
|
|
courseSlug: cs.course_slug,
|
|
},
|
|
query: { courseSessionId: cs.id },
|
|
}"
|
|
data-cy="person-learning-path-link"
|
|
class="link w-full lg:text-right"
|
|
>
|
|
{{ $t("a.Profil anzeigen") }}
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|