Merged in feature/trainer-cockpit-VBV-776--2024-11-14 (pull request #425)

Feature/trainer cockpit VBV-776  2024 11 14

Approved-by: Stéphanie Rotzetter
Approved-by: Elia Bieri
This commit is contained in:
Ramon Wenger 2024-11-27 14:14:05 +00:00
commit 2c4512ba91
26 changed files with 1007 additions and 674 deletions

View File

@ -0,0 +1,58 @@
import { i18nextInit, loadI18nextLocaleMessages } from "@/i18nextWrapper";
import AttendanceStatus from "@/pages/cockpit/cockpitPage/AttendanceStatus.vue";
import { config, mount } from "@vue/test-utils";
import i18next from "i18next";
import I18NextVue from "i18next-vue";
import { expect, vi } from "vitest";
describe("AttendanceStatus.vue", async () => {
vi.useFakeTimers();
const date = new Date(1999, 2, 31);
vi.setSystemTime(date);
await i18nextInit();
await loadI18nextLocaleMessages("de");
config.global.plugins = [[I18NextVue, { i18next }]];
test("Attendance check complete", () => {
const wrapper = mount(AttendanceStatus, {
props: {
done: true,
date: "",
},
});
expect(wrapper.text()).toContain("Du hast die Anwesenheit bestätigt.");
});
test("Attendance check future", () => {
const future = "1999-04-02T06:30:00+00:00";
const wrapper = mount(AttendanceStatus, {
props: {
done: false,
date: future,
},
});
expect(wrapper.text()).toContain("Der Präsenzkurs findet in 2 Tagen statt.");
});
test("Attendance check future", () => {
const future = "1999-04-01T06:30:00+00:00";
const wrapper = mount(AttendanceStatus, {
props: {
done: false,
date: future,
},
});
expect(wrapper.text()).toContain("Der Präsenzkurs findet in einem Tag statt.");
});
test("Attendance check now", () => {
const yesterday = "1999-03-30T06:30:00+00:00";
const wrapper = mount(AttendanceStatus, {
props: {
done: false,
date: yesterday,
},
});
expect(wrapper.text()).toContain("Überprüfe jetzt die Anwesenheit.");
});
});

View File

@ -0,0 +1,16 @@
import { expect, vi } from "vitest";
import { isInFuture } from "../dueDates/dueDatesUtils";
test("Date Utils", () => {
vi.useFakeTimers();
const date = new Date(1999, 2, 31);
vi.setSystemTime(date);
const today = "1999-03-31T06:30:00+00:00";
const yesterday = "1999-03-30T06:30:00+00:00";
const tomorrow = "1999-04-01T06:30:00+00:00";
expect(isInFuture(yesterday)).toBeFalsy();
expect(isInFuture(today)).toBeFalsy();
expect(isInFuture(tomorrow)).toBeTruthy();
});

View File

@ -59,3 +59,12 @@ export const getWeekday = (date: Dayjs) => {
}
return "";
};
export const isInFuture = (date: string) => {
// is today before the prop date?
return dayjs().isBefore(date, "day");
};
export const howManyDaysInFuture = (date: string) => {
return dayjs(date).diff(dayjs().startOf("day"), "day");
};

View File

@ -11,7 +11,7 @@ const { t } = useTranslation();
<template v-if="isInCourse">
<div class="flex h-full items-center border-r border-slate-500">
<router-link to="/" class="flex items-center pr-3">
<it-icon-arrow-left />
<it-icon-arrow-left class="fill-current text-slate-500" />
<span class="hidden text-slate-500 lg:inline">
{{ t("a.Dashboard") }}
</span>

View File

@ -40,7 +40,7 @@ onMounted(() => {
<CourseSessionNavigation />
</div>
<div class="flex items-stretch justify-start space-x-8">
<div class="flex items-stretch justify-start gap-2 lg:gap-4">
<router-link
v-if="hasMediaLibraryMenu"
:to="

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useVVByLink } from "@/composables";
import { SETTINGS_ROUTE } from "@/router/names";
import { PERSONAL_PROFILE_ROUTE, SETTINGS_ROUTE } from "@/router/names";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { User } from "@/stores/user";
import type { CourseSession } from "@/types";
@ -54,13 +54,16 @@ const mentorTabTitle = computed(() =>
const settingsRoute = {
name: SETTINGS_ROUTE,
};
const profileRoute = {
name: PERSONAL_PROFILE_ROUTE,
};
</script>
<template>
<ItFullScreenModal :show="show" @closemodal="emit('closemodal')">
<div>
<div class="-mx-4">
<div>
<div v-if="user?.loggedIn" class="-mx-4 border-b px-8 pb-4">
<div v-if="user?.loggedIn" class="border-b px-8 pb-4">
<div class="-ml-4 flex">
<div v-if="user?.avatar_url">
<img
@ -71,16 +74,27 @@ const settingsRoute = {
</div>
<div class="ml-6">
<h3>{{ user?.first_name }} {{ user?.last_name }}</h3>
<div class="mb-3 text-sm text-gray-800">{{ user.email }}</div>
<router-link
:to="profileRoute"
class="underline"
@click="emit('closemodal')"
>
{{ $t("a.Profil anzeigen") }}
</router-link>
</div>
</div>
</div>
<div>
<div v-if="courseSession" class="mt-6 border-b">
<h4 class="px-4 text-sm text-gray-900">{{ courseSession.course.title }}</h4>
<ul class="mt-6 flex flex-col">
<li v-if="hasCockpitMenu" class="mb-6">
<div v-if="courseSession" class="border-b px-4 py-6">
<h4 class="mb-4 px-4 text-sm text-gray-900">
{{ courseSession.course.title }}
</h4>
<ul class="flex flex-col gap-2">
<li v-if="hasCockpitMenu">
<router-link
class="w-full px-4 py-2"
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-cockpit-link"
:to="getCockpitUrl(courseSession.course.slug)"
@ -89,9 +103,9 @@ const settingsRoute = {
{{ $t("cockpit.title") }}
</router-link>
</li>
<li v-if="hasPreviewMenu" class="mb-2 flex">
<li v-if="hasPreviewMenu">
<router-link
class="w-full px-4 py-2"
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-preview-link"
:to="getLearningPathUrl(courseSession.course.slug)"
@ -100,9 +114,9 @@ const settingsRoute = {
{{ $t("a.Vorschau Teilnehmer") }}
</router-link>
</li>
<li v-if="hasLearningPathMenu" class="mb-2 flex">
<li v-if="hasLearningPathMenu">
<router-link
class="w-full px-4 py-2"
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-learning-path-link"
:to="getLearningPathUrl(courseSession.course.slug)"
@ -111,9 +125,9 @@ const settingsRoute = {
{{ $t("general.learningPath") }}
</router-link>
</li>
<li v-if="hasCompetenceNaviMenu" class="mb-2 flex">
<li v-if="hasCompetenceNaviMenu">
<router-link
class="w-full px-4 py-2"
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-competence-profile-link"
:to="getCompetenceNaviUrl(courseSession.course.slug)"
@ -122,9 +136,9 @@ const settingsRoute = {
{{ $t("competences.title") }}
</router-link>
</li>
<li v-if="hasLearningMentor" class="mb-2 flex">
<li v-if="hasLearningMentor">
<router-link
class="w-full px-4 py-2"
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-mentor-link"
:to="getLearningMentorUrl(courseSession.course.slug)"
@ -147,9 +161,9 @@ const settingsRoute = {
</li>
</ul>
</div>
<div class="mt-6 border-b">
<ul>
<li v-if="courseSession && hasMediaLibraryMenu" class="mb-6 flex">
<div v-if="courseSession" class="border-b px-4">
<ul class="flex flex-col gap-2 py-6">
<li v-if="courseSession && hasMediaLibraryMenu" class="flex">
<router-link
data-cy="medialibrary-link"
class="flex w-full items-center gap-2 px-4 py-2"
@ -161,7 +175,7 @@ const settingsRoute = {
{{ $t("a.Mediathek") }}
</router-link>
</li>
<li v-if="courseSession && hasMediaLibraryMenu" class="mb-6 flex">
<li v-if="courseSession && hasMediaLibraryMenu" class="flex">
<router-link
data-cy="calendar-link"
class="flex w-full items-center gap-2 px-4 py-2"
@ -176,25 +190,28 @@ const settingsRoute = {
</li>
</ul>
</div>
<router-link
:to="settingsRoute"
class="mt-6 flex w-full items-center gap-2 px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
v-if="user?.loggedIn"
type="button"
>
<it-icon-settings />
{{ $t("a.Einstellungen") }}
</router-link>
<button
v-if="user?.loggedIn"
type="button"
class="mt-6 flex items-center px-4 py-2"
@click="$emit('logout')"
>
<it-icon-logout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
</button>
<div class="flex flex-col gap-2 px-4 py-6">
<router-link
v-if="user?.loggedIn"
:to="settingsRoute"
class="flex w-full items-center gap-2 px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
type="button"
>
<it-icon-settings />
{{ $t("a.Einstellungen") }}
</router-link>
<button
v-if="user?.loggedIn"
type="button"
class="flex items-center px-4 py-2"
@click="$emit('logout')"
>
<it-icon-logout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -36,14 +36,13 @@ const showCourseSessionMenu = computed(
v-if="hasSessionTitle"
class="nav-item-base inline-flex items-center lg:inline-flex"
>
<div data-cy="current-course-session-title" class="text-bold">
{{ selectedCourseSessionTitle }}
</div>
<Popover v-if="showCourseSessionMenu" class="relative">
<PopoverButton
class="group flex items-center rounded-md bg-transparent px-3 text-base focus:outline-none"
class="group flex items-center gap-1 rounded-md bg-transparent px-3 text-base focus:outline-none"
>
<span data-cy="current-course-session-title" class="text-bold">
{{ selectedCourseSessionTitle }}
</span>
<it-icon-arrow-down class="h-6 w-6" />
</PopoverButton>
<PopoverPanel class="absolute left-0 z-10 mt-3 w-64 px-1 sm:px-0 lg:max-w-3xl">
@ -59,5 +58,9 @@ const showCourseSessionMenu = computed(
</div>
</PopoverPanel>
</Popover>
<div v-else data-cy="current-course-session-title" class="text-bold">
{{ selectedCourseSessionTitle }}
</div>
</div>
</template>

View File

@ -27,7 +27,7 @@ const isExternalLink = computed(() => {
target="_blank"
>
<slot />
<it-icon-external-link class="w-6" />
<it-icon-external-link class="h-6 w-6" />
</a>
<!-- make `:to` explicit -->
<router-link

View File

@ -9,6 +9,8 @@ interface Props {
items?: DropdownSelectable[];
borderless?: boolean;
placeholderText?: string | null;
asHeading?: boolean; // style the dropdown to be used as a page heading
typeName?: string; // to display the type of the selected item, e.g. `Circle: Fahrzeug` instead of `Fahrzeug`
}
const emit = defineEmits<{
@ -24,6 +26,8 @@ const props = withDefaults(defineProps<Props>(), {
},
items: () => [],
placeholderText: null,
asHeading: false,
typeName: "",
});
const dropdownSelected = computed<DropdownSelectable>({
@ -36,26 +40,34 @@ const dropdownSelected = computed<DropdownSelectable>({
<Listbox v-model="dropdownSelected" as="div">
<div class="relative w-full">
<ListboxButton
class="relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left"
:class="{
border: !props.borderless,
'font-bold': !props.borderless,
}"
:class="[
{
border: !borderless && !asHeading,
'font-bold': !borderless,
},
asHeading
? 'group flex w-full items-center gap-1 rounded-md bg-transparent text-base focus:outline-none'
: 'relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left',
]"
data-cy="dropdown-select"
>
<span v-if="dropdownSelected.iconName" class="mr-4">
<component :is="dropdownSelected.iconName"></component>
</span>
<span class="block truncate">
{{ dropdownSelected.name }}
<span :class="[asHeading ? 'h-11 text-4xl' : '']" class="block truncate">
{{ typeName }} {{ dropdownSelected.name }}
<span v-if="placeholderText && !dropdownSelected.name" class="text-gray-900">
{{ placeholderText }}
</span>
</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
class="pointer-events-none flex items-center pr-2"
:class="asHeading ? '' : 'absolute inset-y-0 right-0'"
>
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
<it-icon-arrow-down
:class="asHeading ? 'h-12 w-12' : 'h-5 w-5'"
aria-hidden="true"
/>
</span>
</ListboxButton>
@ -79,7 +91,7 @@ const dropdownSelected = computed<DropdownSelectable>({
active ? 'bg-blue-900 text-white' : 'text-black',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
class="flex flex-row items-center"
class="group flex flex-row items-center"
:data-cy="`dropdown-select-option-${item.name}`"
>
<span v-if="item.iconName" class="mr-4">
@ -98,7 +110,11 @@ const dropdownSelected = computed<DropdownSelectable>({
v-if="dropdownSelected"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-blue-900"
>
<it-icon-check v-if="selected" class="h-5 w-5" aria-hidden="true" />
<it-icon-check
v-if="selected"
class="h-5 w-5 fill-current group-hover:text-white"
aria-hidden="true"
/>
</span>
</li>
</ListboxOption>

View File

@ -34,6 +34,7 @@
"a.An Durchführung teilnehmen": "An Durchführung teilnehmen",
"a.Anmelden": "Anmelden",
"a.Anwesenheit": "Anwesenheit",
"a.Anwesenheit anschauen": "Anwesenheit anschauen",
"a.Anwesenheit Präsenzkurse": "Anwesenheit Präsenzkurse",
"a.Anwesenheitskontrolle Präsenzkurse": "Anwesenheitskontrolle Präsenzkurse",
"a.Arbeiten": "Arbeiten",
@ -84,6 +85,8 @@
"a.Deine Änderungen wurden gespeichert": "Deine Änderungen wurden gespeichert",
"a.Der Lehrgang und die Prüfung zum Erwerb des Verbandszertifikats als Versicherungsvermittler/-in.": "Der Lehrgang und die Prüfung zum Erwerb des Verbandszertifikats als Versicherungsvermittler/-in.",
"a.Der Preis für den Lehrgang {course} beträgt {price}.": "Der Preis für den Lehrgang {course} beträgt {price} exkl. MWSt.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._one": "Der Präsenzkurs findet in einem Tag statt.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._other": "Der Präsenzkurs findet in {{count}} Tagen statt.",
"a.Details anschauen": "Details anschauen",
"a.Details anzeigen": "Details anzeigen",
"a.Deutsch": "Deutsch",
@ -95,6 +98,7 @@
"a.Du hast alles erledigt.": "Du hast alles erledigt.",
"a.Du hast deine Fremdeinschätzung freigegeben": "Du hast deine Fremdeinschätzung freigegeben.",
"a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.": "Du hast deine Selbsteinschätzung erfolgreich mit {{FULL_NAME}} geteilt.",
"a.Du hast die Anwesenheit bestätigt.": "Du hast die Anwesenheit bestätigt.",
"a.Du hast die Einladung von {name} erfolgreich akzeptiert.": "Du hast die Einladung von {name} erfolgreich akzeptiert.",
"a.Du hast erfolgreich ein Konto für EMAIL erstellt.": "Du hast erfolgreich ein Konto für {{email}} erstellt.",
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt.": "Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt.",
@ -310,6 +314,7 @@
"a.Überbetriebliche Kurse": "Überbetriebliche Kurse",
"a.Übergangslösung Innendienst-Mitarbeitende": "Übergangslösung Innendienst-Mitarbeitende",
"a.Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für FEEDBACK_REQUESTER frei": "Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für {{FEEDBACK_REQUESTER}} frei.",
"a.Überprüfe jetzt die Anwesenheit.": "Überprüfe jetzt die Anwesenheit.",
"a.Übersicht": "Übersicht",
"a.Übersicht anschauen": "Übersicht anschauen",
"Abgabe": "Abgabe",

View File

@ -34,6 +34,7 @@
"a.An Durchführung teilnehmen": "Participer à la session",
"a.Anmelden": "Connexion",
"a.Anwesenheit": "Présence",
"a.Anwesenheit anschauen": "Voir le contrôle de présence",
"a.Anwesenheit Präsenzkurse": "Présence aux cours",
"a.Anwesenheitskontrolle Präsenzkurse": "Contrôle de présence aux cours",
"a.Arbeiten": "Travaux",
@ -84,6 +85,8 @@
"a.Deine Änderungen wurden gespeichert": "Tes modifications ont été enregistrées",
"a.Der Lehrgang und die Prüfung zum Erwerb des Verbandszertifikats als Versicherungsvermittler/-in.": "Le cours et l'examen pour obtenir le certificat d'association comme courtier/agent d'assurance.",
"a.Der Preis für den Lehrgang {course} beträgt {price}.": "Le prix de la formation {course} est de {price} hors TVA.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._one": "Le cours de présence se déroule en une jour.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._other": "Le cours de présence se déroule en {{count}} jours.",
"a.Details anschauen": "Voir les détails",
"a.Details anzeigen": "Afficher les détails",
"a.Deutsch": "Allemand",
@ -95,6 +98,7 @@
"a.Du hast alles erledigt.": "Tu as tout fini.",
"a.Du hast deine Fremdeinschätzung freigegeben": "Tu as autorisé ton évaluation externe.",
"a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.": "Tu as partagé avec succès ton auto-évaluation avec {{FULL_NAME}}.",
"a.Du hast die Anwesenheit bestätigt.": "Tu as confirmé la présence.",
"a.Du hast die Einladung von {name} erfolgreich akzeptiert.": "Tu as accepté avec succès l'invitation de {name}.",
"a.Du hast erfolgreich ein Konto für EMAIL erstellt.": "Vous avez créé un compte avec succès pour {{email}}.",
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt.": "Tu peux partager ton auto-évaluation avec ton accompagnateur d'apprentissage afin qu'il puisse effectuer une évaluation externe.",
@ -310,6 +314,7 @@
"a.Überbetriebliche Kurse": "Cours interentreprises",
"a.Übergangslösung Innendienst-Mitarbeitende": "Solution transitoire pour le service interne",
"a.Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für FEEDBACK_REQUESTER frei": "Vérifie tes saisies ci-dessous et libère ensuite ton évaluation externe pour {{FEEDBACK_REQUESTER}}.",
"a.Überprüfe jetzt die Anwesenheit.": "Vérifie maintenant la présence.",
"a.Übersicht": "Aperçu",
"a.Übersicht anschauen": "Consulter l'aperçu",
"Abgabe": "Remise",

View File

@ -8,7 +8,6 @@
"a.Abgabetermin": "Termine di consegna",
"a.Abgezogene Punkte": "Punti detratti",
"a.Adresse": "Indirizzo",
"a.AGB": "Condizioni generali",
"a.Aktuell begleitest du niemanden als Lernbegleitung.": "Attualmente non stai accompagnando nessuno come mentore di apprendimento.",
"a.Aktuell begleitest du niemanden als Praxisbildner.": "Attualmente non stai accompagnando nessuno come formatore/-trice pratico/a.",
"a.Aktuell bist du leider keiner Durchführung zugewiesen.": "Attualmente non sei assegnato a nessuna sessione, purtroppo.",
@ -34,6 +33,7 @@
"a.An Durchführung teilnehmen": "Partecipare alla sessione",
"a.Anmelden": "Login",
"a.Anwesenheit": "Presenza",
"a.Anwesenheit anschauen": "Visualizza il controllo di presenza",
"a.Anwesenheit Präsenzkurse": "Presenza ai corsi",
"a.Anwesenheitskontrolle Präsenzkurse": "Controllo di presenza ai corsi",
"a.Arbeiten": "Lavori",
@ -74,7 +74,6 @@
"a.Datei auswählen": "Selezionare il file",
"a.Datei hochladen": "Carica il file",
"a.Datei kann nicht gespeichert werden.": "Impossibile salvare il file.",
"a.Datenschutzerklärung": "Informativa sulla privacy",
"a.Datum": "Data",
"a.Debit-/Kreditkarte/Twint": "Carta di debito/credito / Twint",
"a.Dein Feedback für x y wurde freigegeben.": "Il tuo feedback per {{x}} {{y}} è stato pubblicato.",
@ -83,7 +82,9 @@
"a.Deine Selbsteinschätzung": "La tua autovalutazione",
"a.Deine Änderungen wurden gespeichert": "Le tue modifiche sono state salvate",
"a.Der Lehrgang und die Prüfung zum Erwerb des Verbandszertifikats als Versicherungsvermittler/-in.": "Il corso e l'esame per ottenere il certificato di associazione come intermediario/agente di assicurazione.",
"a.Der Preis für den Lehrgang {course} beträgt {price}.": "Il prezzo del {course} è {price} IVA esclusa.",
"a.Der Preis für den Lehrgang {course} beträgt {price}.": "Il prezzo del {corso} è {prezzo} IVA esclusa.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._one": "Il corso di presenza si svolge in un giorno.",
"a.Der Präsenzkurs findet in {{days}} Tagen statt._other": "Il corso di presenza si svolge in {{count}} giorni.",
"a.Details anschauen": "Visualizza dettagli",
"a.Details anzeigen": "Mostrare i dettagli",
"a.Deutsch": "Tedesco",
@ -95,6 +96,7 @@
"a.Du hast alles erledigt.": "Hai fatto tutto.",
"a.Du hast deine Fremdeinschätzung freigegeben": "Hai rilasciato la tua valutazione esterna.",
"a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.": "Hai condiviso con successo la tua autovalutazione con {{FULL_NAME}}.",
"a.Du hast die Anwesenheit bestätigt.": "Hai confermato la presenza.",
"a.Du hast die Einladung von {name} erfolgreich akzeptiert.": "Hai accettato con successo l'invito di {name}.",
"a.Du hast erfolgreich ein Konto für EMAIL erstellt.": "Hai creato con successo un account per {{email}}.",
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt.": "Puoi condividere la tua autovalutazione con il tuo tutor didattico affinché possa effettuare una valutazione esterna.",
@ -117,7 +119,6 @@
"a.Ergebnisse bewerten": "Valutare i risultati",
"a.Ergebnisse teilen": "Condividere i risultati",
"a.Erneut bearbeiten": "Modifica di nuovo",
"a.Es gelten die {cembraTos} und die {cembraPrivacy} der CembraPay AG.": "Si applicano le {cembraTos} e l'{cembraPrivacy} di CembraPay AG.",
"a.Experte": "Esperto",
"a.Feedback abschliessen": "Completa il feedback",
"a.Feedback ansehen": "Visualizza il feedback",
@ -213,7 +214,6 @@
"a.Nicht bestanden": "Non superato",
"a.Nicht bewertet": "Non valutato",
"a.Nichtleben": "Non vita",
"a.Noch nicht bestätigt": "Non ancora confermato",
"a.Note": "Note",
"a.NUMBER Elemente abgeschlossen": "{NUMBER} elementi completati",
"a.NUMBER Präsenztage abgeschlossen": "{NUMBER} giorni di presenza completati",
@ -225,7 +225,6 @@
"a.Personen, die du begleitest": "Persone che accompagni",
"a.Persönliche Informationen": "Informazioni personali",
"a.PLZ": "CAP",
"a.Postleizahl hat das falsche Format": "Il codice postale ha un formato sbagliato",
"a.Praxisauftrag": "Lavoro pratico",
"a.Praxisaufträge anschauen": "Visualizzare gli incarichi pratici",
"a.Praxisbildner": "Formatore pratico",
@ -270,7 +269,6 @@
"a.Teilnehmer": "Partecipanti",
"a.Teilnehmer im": "I partecipanti al",
"a.Teilnehmer nach Zulassungsprofilen im": "Partecipanti per profilo di ammissione nel",
"a.Teilnehmer Vorschau": "Anteprima dei partecipanti",
"a.Telefonnummer": "Numero di telefono",
"a.Telefonnummer hat das falsche Format": "Il numero di telefono ha un formato sbagliato",
"a.Termin": "Data",
@ -310,6 +308,7 @@
"a.Überbetriebliche Kurse": "Corsi interaziendali",
"a.Übergangslösung Innendienst-Mitarbeitende": "Soluzione transitoria per il servizio interno",
"a.Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für FEEDBACK_REQUESTER frei": "Controlla le tue voci qui sotto e poi rilascia la tua valutazione esterna per {{FEEDBACK_REQUESTER}}.",
"a.Überprüfe jetzt die Anwesenheit.": "Controllare la presenza ora.",
"a.Übersicht": "Panoramica",
"a.Übersicht anschauen": "Vedere la panoramica",
"Abgabe": "Consegna",
@ -357,8 +356,6 @@
"Berufsbildner": "Formatore professionale",
"Bestanden": "Superato",
"Bewertung von x y": "Valutazione di {{x}} {{y}}",
"cembraPrivacyLink": "https://cembrapay.ch/it/privacy",
"cembraTosLink": "https://cembrapay.ch/it/terms/CP",
"Circle": "Cerchio",
"circlePage.circleContentBoxTitle": "Cosa apprenderai in questo Circle",
"circlePage.contactExpertButton": "Contattare il/la trainer",

View File

@ -1,27 +1,30 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import type { AttendanceUserStatus } from "@/gql/graphql";
import { graphqlClient } from "@/graphql/client";
import type {
AttendanceUserStatus,
CourseSessionAttendanceCourseObjectType,
} from "@/gql/graphql";
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { exportAttendance } from "@/services/dashboard";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import { useUserStore } from "@/stores/user";
import type { DropdownSelectable } from "@/types";
import { openDataAsXls } from "@/utils/export";
import { useMutation } from "@urql/vue";
import dayjs from "dayjs";
import { useMutation, useQuery } from "@urql/vue";
import { useDateFormat } from "@vueuse/core";
import { useTranslation } from "i18next-vue";
import log from "loglevel";
import { computed, onMounted, reactive, watch } from "vue";
import { computed, onMounted, ref } from "vue";
import AttendanceCheck from "../cockpitPage/AttendanceCheck.vue";
import AttendanceStatus from "../cockpitPage/AttendanceStatus.vue";
const { t } = useTranslation();
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
const courseSessionDetailResult = useCourseSessionDetailQuery();
const userStore = useUserStore();
const courseSession = useCurrentCourseSession();
const expertCockpitStore = useExpertCockpitStore();
const attendanceCourses = computed(() => {
return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
@ -31,46 +34,18 @@ const courseSessionDetail = computed(() => {
return courseSessionDetailResult.courseSessionDetail.value;
});
const attendanceCourseCircleId = computed(() => {
const selectedAttendandeCourse = attendanceCourses.value.find(
(course) => course.id === state.attendanceCourseSelected.id
);
return selectedAttendandeCourse?.learning_content?.circle?.id;
});
const currentCourse = computed(() => expertCockpitStore.currentCourse);
const presenceCoursesDropdownOptions = computed(() => {
return attendanceCourses.value.map(
(attendanceCourse) =>
({
id: attendanceCourse.id,
name: `${t("a.Präsenzkurs")} ${
attendanceCourse.learning_content.circle?.title
} ${dayjs(attendanceCourse.due_date?.start).format("DD.MM.YYYY")}`,
}) as DropdownSelectable
);
});
const userPresence = ref(new Map<string, boolean>());
const disclaimerConfirmed = ref(false);
const attendanceSaved = ref(false);
const state = reactive({
userPresence: new Map<string, boolean>(),
attendanceCourseSelected: presenceCoursesDropdownOptions.value[0],
disclaimerConfirmed: false,
attendanceSaved: false,
});
watch(
attendanceCourses,
(newVal) => {
if (newVal && newVal.length > 0) {
state.attendanceCourseSelected = presenceCoursesDropdownOptions.value[0];
}
},
{ immediate: true }
);
const { t } = useTranslation();
function resetState() {
state.userPresence = new Map<string, boolean>();
state.disclaimerConfirmed = false;
state.attendanceSaved = false;
userPresence.value = new Map<string, boolean>();
disclaimerConfirmed.value = false;
attendanceSaved.value = false;
}
const onSubmit = async () => {
@ -78,78 +53,89 @@ const onSubmit = async () => {
user_id: string;
status: AttendanceUserStatus;
};
const attendanceUserList: UserPresence[] = Array.from(state.userPresence.keys()).map(
const attendanceUserList: UserPresence[] = Array.from(userPresence.value.keys()).map(
(key) => ({
user_id: key,
status: state.userPresence.get(key) ? "PRESENT" : "ABSENT",
status: userPresence.value.get(key) ? "PRESENT" : "ABSENT",
})
);
const res = await attendanceMutation.executeMutation({
attendanceCourseId: state.attendanceCourseSelected.id.toString(),
attendanceCourseId: (
currentCourse.value as CourseSessionAttendanceCourseObjectType
).id.toString(),
attendanceUserList: attendanceUserList,
});
if (res.error) {
log.error("Could not submit attendance check: ", res.error);
return;
}
state.disclaimerConfirmed = false;
state.attendanceSaved = true;
disclaimerConfirmed.value = false;
attendanceSaved.value = true;
log.info("Attendance check submitted: ", res);
};
const loadAttendanceData = async () => {
resetState();
// with changing variables `useQuery` does not seem to work correctly
if (state.attendanceCourseSelected) {
const res = await graphqlClient.query(
ATTENDANCE_CHECK_QUERY,
{
courseSessionId: state.attendanceCourseSelected.id.toString(),
if (currentCourse.value) {
const result = await useQuery({
query: ATTENDANCE_CHECK_QUERY,
variables: {
courseSessionId: currentCourse.value.id.toString(),
},
{
requestPolicy: "network-only",
}
);
requestPolicy: "network-only",
});
const attendanceUserList =
res.data?.course_session_attendance_course?.attendance_user_list ?? [];
result.data?.value?.course_session_attendance_course?.attendance_user_list ?? [];
for (const user of attendanceUserList) {
if (!user) continue;
state.userPresence.set(user.user_id, user.status === "PRESENT");
userPresence.value.set(user.user_id, user.status === "PRESENT");
}
if (attendanceUserList.length !== 0) {
state.attendanceSaved = true;
attendanceSaved.value = true;
}
}
};
function editAgain() {
state.attendanceSaved = false;
attendanceSaved.value = false;
}
const toggleDisclaimer = (newValue: boolean) => {
disclaimerConfirmed.value = newValue;
};
async function exportData() {
const data = await exportAttendance(
{
courseSessionIds: [Number(courseSession.value.id)],
circleIds: [Number(attendanceCourseCircleId.value)],
circleIds: [Number(currentCourse.value?.learning_content.circle?.id)],
},
userStore.language
);
openDataAsXls(data.encoded_data, data.file_name);
}
onMounted(() => {
onMounted(async () => {
log.debug("AttendanceCheckPage mounted");
loadAttendanceData();
});
watch(
() => state.attendanceCourseSelected,
() => {
log.debug("attendanceCourseSelected changed", state.attendanceCourseSelected);
loadAttendanceData();
},
{ immediate: true }
);
const courseDueDate = computed(() => {
if (currentCourse.value && currentCourse.value.due_date?.start) {
return currentCourse.value.due_date.start;
}
return "";
});
const formattedCourseDueDate = computed(() => {
if (courseDueDate.value) {
return useDateFormat(courseDueDate.value, "D. MMMM YYYY", {
locales: "de-CH",
});
}
return "";
});
</script>
<template>
@ -164,66 +150,50 @@ watch(
<span>{{ $t("general.back") }}</span>
</router-link>
</nav>
<div class="flex items-center justify-between">
<h3 class="pb-4 text-xl font-bold">{{ $t("a.Anwesenheit Präsenzkurse") }}</h3>
<button
v-if="state.attendanceSaved"
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>
<section v-if="attendanceCourses.length && state.attendanceCourseSelected">
<div class="flex flex-row flex-wrap justify-between bg-white p-6">
<ItDropdownSelect
v-model="state.attendanceCourseSelected"
:items="presenceCoursesDropdownOptions ?? []"
></ItDropdownSelect>
<div
v-if="!state.attendanceSaved"
class="flex flex-row flex-wrap items-center space-y-2 md:space-y-0"
<div class="flex items-center justify-between"></div>
<section v-if="attendanceCourses.length && currentCourse">
<div class="grid grid-cols-[2fr_1fr] justify-between gap-8 bg-white py-6">
<div class="col-span-1 flex flex-col gap-2 px-6">
<h3 class="pb-1 text-4xl font-bold">{{ $t("a.Präsenzkurs") }}</h3>
<h5>
{{ t("a.Circle") }} «{{ currentCourse?.learning_content.circle?.title }}»
</h5>
<h5>{{ formattedCourseDueDate }}</h5>
</div>
<button
v-if="attendanceSaved"
class="col-span-1 mr-4 hidden justify-self-end lg:flex"
data-cy="export-button"
@click="exportData"
>
<ItCheckbox
:checkbox-item="{
value: true,
checked: state.disclaimerConfirmed,
}"
@toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
></ItCheckbox>
<p class="w-64 pr-4 text-sm">
{{
$t(
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
)
}}
</p>
<button
class="btn-primary"
:disabled="!state.disclaimerConfirmed"
@click="onSubmit"
>
{{ $t("Anwesenheit bestätigen") }}
</button>
</div>
<div v-else class="self-center">
<p class="text-base">
{{ $t("a.Die Anwesenheit wurde definitiv bestätigt") }}
</p>
<button class="btn-link link" @click="editAgain()">
{{ $t("a.Erneut bearbeiten") }}
</button>
</div>
</div>
<it-icon-export class="fill-current text-blue-900"></it-icon-export>
<span class="ml inline-block text-blue-900">
{{ $t("a.Als Excel exportieren") }}
</span>
</button>
<div
class="col-span-2 flex flex-col items-start gap-4 px-6 lg:gap-6"
:class="attendanceSaved ? 'lg:flex-row lg:items-center' : 'gap-8 lg:gap-8'"
>
<AttendanceStatus
class="inline-flex px-6"
:done="attendanceSaved"
:date="courseDueDate"
/>
<div class="mt-4 flex flex-col bg-white p-6">
<div
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
>
<AttendanceCheck
:attendance-saved="attendanceSaved"
:disclaimer-confirmed="disclaimerConfirmed"
@reopen="editAgain"
@toggle="toggleDisclaimer"
@confirm="onSubmit"
/>
</div>
<div class="col-span-2 border-t border-gray-500 px-6">
<ItPersonRow
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
:class="0 === index ? 'border-none' : ''"
@ -233,16 +203,13 @@ watch(
>
<template #leading>
<ItCheckbox
:disabled="state.attendanceSaved"
:disabled="attendanceSaved"
:checkbox-item="{
value: true,
checked: state.userPresence.get(csu.user_id) as boolean,
checked: userPresence.get(csu.user_id) as boolean,
}"
@toggle="
state.userPresence.set(
csu.user_id,
!state.userPresence.get(csu.user_id)
)
userPresence.set(csu.user_id, !userPresence.get(csu.user_id))
"
></ItCheckbox>
</template>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
export interface Props {
attendanceSaved: boolean;
disclaimerConfirmed: boolean;
}
defineProps<Props>();
defineEmits(["toggle", "reopen", "confirm"]);
</script>
<template>
<div v-if="!attendanceSaved" class="flex flex-col gap-4">
<div class="flex flex-row content-center items-center">
<ItCheckbox
:checkbox-item="{
value: true,
checked: disclaimerConfirmed,
}"
@toggle="$emit('toggle', !disclaimerConfirmed)"
></ItCheckbox>
<p class="text-sm">
{{
$t(
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
)
}}
</p>
</div>
<button
class="btn-primary w-64"
:disabled="!disclaimerConfirmed"
@click="$emit('confirm')"
>
{{ $t("Anwesenheit bestätigen") }}
</button>
</div>
<div v-else class="flex-inline">
<button class="btn-link link" @click="$emit('reopen')">
{{ $t("a.Erneut bearbeiten") }}
</button>
</div>
</template>

View File

@ -0,0 +1,81 @@
<script lang="ts" setup>
import type { CourseSessionAttendanceCourseObjectType } from "@/gql/graphql";
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { ATTENDANCE_ROUTE } from "@/router/names";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import { getStatus } from "@/utils/attendance";
import { useQuery } from "@urql/vue";
import { useDateFormat } from "@vueuse/core";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
import AttendanceStatus from "./AttendanceStatus.vue";
const attendanceRoute = {
name: ATTENDANCE_ROUTE,
};
const { t } = useTranslation();
const expertCockpitStore = useExpertCockpitStore();
const currentCourse = computed(() => expertCockpitStore.currentCourse);
const shouldPause = computed(() => !currentCourse.value);
const result = useQuery({
query: ATTENDANCE_CHECK_QUERY,
variables: () => ({
courseSessionId: (
currentCourse.value as CourseSessionAttendanceCourseObjectType
).id.toString(),
}),
pause: shouldPause,
});
// todo: maybe we can move these next 3 computed values somewhere else, as they are also used in the AttendanceCheckPage component
const courseDueDate = computed(() => {
if (currentCourse.value && currentCourse.value.due_date?.start) {
return currentCourse.value.due_date.start;
}
return "";
});
const attendanceSaved = computed(() => {
const attendanceUserList =
result.data?.value?.course_session_attendance_course?.attendance_user_list ?? [];
return attendanceUserList.length !== 0;
});
const formattedCourseDueDate = computed(() => {
if (courseDueDate.value) {
return useDateFormat(courseDueDate.value, "D. MMMM YYYY", {
locales: "de-CH",
});
}
return "";
});
const status = computed(() => {
return getStatus(attendanceSaved.value, courseDueDate.value);
});
</script>
<template>
<div
class="my-4 flex flex-col items-start justify-between gap-4 bg-white p-6 lg:my-0 lg:flex-row lg:items-center lg:gap-0"
>
<div>
<h2 class="text-base font-bold">{{ t("a.Präsenzkurs") }}</h2>
<p class="text-sm text-gray-800">{{ formattedCourseDueDate }}</p>
</div>
<AttendanceStatus :date="courseDueDate" :done="attendanceSaved" />
<router-link
:to="attendanceRoute"
:class="
status === 'now' ? 'bg-blue-900 px-4 py-2 font-bold text-white' : 'underline'
"
>
<template v-if="status === 'now'">
{{ $t("Anwesenheit prüfen") }}
</template>
<template v-else>{{ $t("a.Anwesenheit anschauen") }}</template>
</router-link>
</div>
</template>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { howManyDaysInFuture } from "@/components/dueDates/dueDatesUtils";
import { getStatus } from "@/utils/attendance";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
export interface Props {
done: boolean;
date: string;
}
const { t } = useTranslation();
const props = defineProps<Props>();
const status = computed(() => {
return getStatus(props.done, props.date);
});
const style = computed(() => {
switch (status.value) {
case "done":
return "bg-green-200";
case "soon":
return "bg-gray-200";
case "now":
default:
return "bg-sky-200";
}
});
const icon = computed(() => {
switch (status.value) {
case "done":
return "it-icon-check";
case "soon":
case "now":
default:
return "it-icon-info";
}
});
const days = computed(() => {
return howManyDaysInFuture(props.date);
});
const text = computed(() => {
switch (status.value) {
case "done":
return t("a.Du hast die Anwesenheit bestätigt.");
case "soon":
return t("a.Der Präsenzkurs findet in {{days}} Tagen statt.", {
count: days.value,
});
case "now":
default:
return t("a.Überprüfe jetzt die Anwesenheit.");
}
});
</script>
<template>
<div
class="space-between inline-flex flex-row items-center gap-1 rounded py-1 pl-2 pr-4"
:class="style"
>
<component :is="icon" class="h-7 w-7" />
<p>{{ text }}</p>
</div>
</template>

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import AttendanceOverview from "./AttendanceOverview.vue";
const expertCockpitStore = useExpertCockpitStore();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const props = defineProps<{
courseSlug: string;
}>();
</script>
<template>
<div v-if="expertCockpitStore.circles?.length">
<div v-if="expertCockpitStore.currentCircle" class="container-large pt-10">
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<ItDropdownSelect
:as-heading="true"
:model-value="expertCockpitStore.currentCircle"
type-name="Circle:"
class="mt-4 w-full lg:mt-0 lg:w-auto"
:items="expertCockpitStore.circles"
@update:model-value="expertCockpitStore.setCurrentCourseCircleFromEvent"
></ItDropdownSelect>
</div>
<!-- Status -->
<div class="mb-4 gap-4">
<AttendanceOverview />
</div>
<div class="mb-4 bg-white p-6">
<CourseSessionDueDatesList
:course-session-id="courseSession.id"
:circle-id="expertCockpitStore.currentCircle.id"
:max-count="4"
></CourseSessionDueDatesList>
</div>
<SubmissionsOverview
:course-session="courseSession"
:selected-circle="expertCockpitStore.currentCircle.id"
></SubmissionsOverview>
<div class="pt-4">
<!-- progress -->
<div
v-if="courseSessionDetailResult.filterMembers().length > 0"
class="bg-white p-6"
>
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul>
<ItPersonRow
v-for="csu in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
>
<template #center>
<div
class="mt-2 flex w-full flex-col items-center justify-start lg:mt-0 lg:flex-row"
>
<LearningPathDiagram
:course-session-id="courseSession.id"
:course-slug="props.courseSlug"
:user-id="csu.user_id"
:show-circle-slugs="[expertCockpitStore.currentCircle.slug]"
diagram-type="singleSmall"
class="mr-4"
></LearningPathDiagram>
<p class="lg:min-w-[150px]">
{{ expertCockpitStore.currentCircle.title }}
</p>
<UserStatusCount
:course-slug="props.courseSlug"
:user-id="csu.user_id"
></UserStatusCount>
</div>
</template>
<template #link>
<router-link
:to="{
name: 'profileLearningPath',
params: { userId: csu.user_id, courseSlug: props.courseSlug },
}"
class="link w-full lg:text-right"
>
{{ $t("general.profileLink") }}
</router-link>
</template>
</ItPersonRow>
</ul>
</div>
</div>
</div>
<div v-else class="container-large mt-4">
<!-- No circle selected -->
<span class="text-lg text-orange-600">
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
</span>
</div>
</div>
<div v-else class="container-large mt-4">
<span class="text-lg text-orange-600">
<!-- No circle at all (should never happen, mostly
for us to reduce confusion why the cockpit is just empty...) -->
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
</span>
</div>
</template>

View File

@ -1,186 +0,0 @@
<script setup lang="ts">
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import log from "loglevel";
const props = defineProps<{
courseSlug: string;
}>();
log.debug("CockpitIndexPage created", props.courseSlug);
const { loading } = useExpertCockpitPageData(props.courseSlug);
const expertCockpitStore = useExpertCockpitStore();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
</script>
<template>
<div v-if="!loading" class="bg-gray-200">
<div v-if="expertCockpitStore.circles?.length">
<div v-if="expertCockpitStore.currentCircle" class="container-large">
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1>Cockpit</h1>
<ItDropdownSelect
:model-value="expertCockpitStore.currentCircle"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="expertCockpitStore.circles"
@update:model-value="expertCockpitStore.setCurrentCourseCircleFromEvent"
></ItDropdownSelect>
</div>
<!-- Status -->
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("Trainerunterlagen") }}
</h3>
<div class="mb-4">
{{ $t("cockpit.trainerFilesText") }}
</div>
</div>
<div>
<a
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
class="btn-secondary min-w-min"
target="_blank"
>
{{ $t("MS Teams öffnen") }}
</a>
</div>
</div>
<div
v-if="courseSession.course.configuration.enable_circle_documents"
class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"
data-cy="circle-documents"
>
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("a.Unterlagen für Teilnehmenden") }}
</h3>
<div class="mb-4">
{{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
</div>
</div>
<div>
<router-link
:to="`/course/${props.courseSlug}/cockpit/documents`"
class="btn-secondary min-w-min"
>
{{ $t("a.Zum Unterlagen-Upload") }}
</router-link>
</div>
</div>
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("a.Anwesenheitskontrolle Präsenzkurse") }}
</h3>
<div class="mb-4">
{{
$t(
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
)
}}
</div>
</div>
<div>
<router-link
:to="`/course/${props.courseSlug}/cockpit/attendance`"
class="btn-secondary min-w-min"
>
{{ $t("Anwesenheit prüfen") }}
</router-link>
</div>
</div>
</div>
<div class="mb-4 bg-white p-6">
<CourseSessionDueDatesList
:course-session-id="courseSession.id"
:circle-id="expertCockpitStore.currentCircle.id"
:max-count="4"
></CourseSessionDueDatesList>
</div>
<SubmissionsOverview
:course-session="courseSession"
:selected-circle="expertCockpitStore.currentCircle.id"
></SubmissionsOverview>
<div class="pt-4">
<!-- progress -->
<div
v-if="courseSessionDetailResult.filterMembers().length > 0"
class="bg-white p-6"
>
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul>
<ItPersonRow
v-for="csu in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
>
<template #center>
<div
class="mt-2 flex w-full flex-col items-center justify-start lg:mt-0 lg:flex-row"
>
<LearningPathDiagram
:course-session-id="courseSession.id"
:course-slug="props.courseSlug"
:user-id="csu.user_id"
:show-circle-slugs="[expertCockpitStore.currentCircle.slug]"
diagram-type="singleSmall"
class="mr-4"
></LearningPathDiagram>
<p class="lg:min-w-[150px]">
{{ expertCockpitStore.currentCircle.title }}
</p>
<UserStatusCount
:course-slug="props.courseSlug"
:user-id="csu.user_id"
></UserStatusCount>
</div>
</template>
<template #link>
<router-link
:to="{
name: 'profileLearningPath',
params: { userId: csu.user_id, courseSlug: props.courseSlug },
}"
class="link w-full lg:text-right"
>
{{ $t("general.profileLink") }}
</router-link>
</template>
</ItPersonRow>
</ul>
</div>
</div>
</div>
<div v-else class="container-large mt-4">
<!-- No circle selected -->
<span class="text-lg text-orange-600">
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
</span>
</div>
</div>
<div v-else class="container-large mt-4">
<span class="text-lg text-orange-600">
<!-- No circle at all (should never happen, mostly
for us to reduce confusion why the cockpit is just empty...) -->
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
</span>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import SubNavigation from "@/components/header/SubNavigation.vue";
import { useCurrentCourseSession } from "@/composables";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import { COCKPIT_ROUTE, DOCUMENTS_ROUTE } from "@/router/names";
import { useTranslation } from "i18next-vue";
import log from "loglevel";
import { computed } from "vue";
const props = defineProps<{
courseSlug: string;
}>();
log.debug("CockpitIndexPage created", props.courseSlug);
const { loading } = useExpertCockpitPageData(props.courseSlug);
const defaultRoute = {
name: COCKPIT_ROUTE,
};
// const attendanceRoute = {
// name: ATTENDANCE_ROUTE,
// };
const documentsRoute = {
name: DOCUMENTS_ROUTE,
};
const { t } = useTranslation();
const courseSession = useCurrentCourseSession();
const enableDocuments = computed(() => {
return !!courseSession.value.course.configuration.enable_circle_documents;
});
const items = computed(() => [
{ id: 1, name: t("a.Übersicht"), route: defaultRoute },
// { id: 2, name: t("a.Teilnehmer"), route: attendanceRoute }, // todo: re-enable with correct route in a later issue
...(enableDocuments.value
? [
{
id: 3,
name: t("a.Unterlagen"),
route: documentsRoute,
dataCy: "circle-documents",
},
]
: []),
{
id: 4,
name: "Vorschau Teilnehmer",
route: "https://iterativ.ch",
},
{
id: 5,
name: "MS Teams",
route: "https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI",
},
]);
</script>
<template>
<div v-if="!loading" class="bg-gray-200">
<SubNavigation :items="items" />
<router-view />
</div>
</template>
<style scoped></style>

View File

@ -136,21 +136,13 @@ async function uploadDocument(data: DocumentUploadData) {
<template>
<div class="bg-gray-200">
<div v-if="courseSession" class="container-large">
<nav class="py-4 pb-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/course/${courseSession.course.slug}/cockpit`"
>
<it-icon-arrow-left />
<span>{{ t("general.back") }}</span>
</router-link>
</nav>
<main>
<main class="py-4">
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h2>{{ t("a.Unterlagen für Teilnehmenden") }}</h2>
<ItDropdownSelect
:model-value="cockpitStore.currentCircle"
class="mt-4 w-full lg:mt-0 lg:w-96"
class="mt-4 w-full lg:mt-0 lg:w-auto"
:as-heading="true"
type-name="Circle:"
:items="cockpitStore.circles"
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
></ItDropdownSelect>

View File

@ -56,17 +56,6 @@ const items: SubNavEntry[] = [
route: selfEvaluationRoute,
},
{ id: 3, name: t("a.Handlungskompetenzen"), route: competencesRoute },
{
id: 4,
name: "MS Teams",
route: "https://iterativ.ch",
},
{
id: 5,
name: "Vorschau Teilnehmer",
route: "https://iterativ.ch",
},
];
</script>

View File

@ -14,9 +14,13 @@ import { addToHistory, setLastNavigationWasPush } from "@/router/history";
import { onboardingRedirect } from "@/router/onboarding";
import { createRouter, createWebHistory } from "vue-router";
import {
ATTENDANCE_ROUTE,
CERTIFICATES_ROUTE,
COCKPIT_ROUTE,
COMPETENCE_ROUTE,
COMPETENCES_ROUTE,
DOCUMENTS_ROUTE,
PERSONAL_PROFILE_ROUTE,
SELF_EVALUATION_ROUTE,
SETTINGS_ROUTE,
} from "./names";
@ -86,88 +90,269 @@ const router = createRouter({
props: true,
},
{
path: "/course/:courseSlug/media",
path: "/course/:courseSlug",
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryParentPage.vue"),
children: [
{
path: "",
component: () => import("@/pages/mediaLibrary/MediaLibraryIndexPage.vue"),
path: "media",
component: () => import("@/pages/mediaLibrary/MediaLibraryParentPage.vue"),
props: true,
children: [
{
path: "",
component: () => import("@/pages/mediaLibrary/MediaLibraryIndexPage.vue"),
props: true,
},
{
path: ":categorySlug",
props: true,
component: () =>
import("@/pages/mediaLibrary/MediaLibraryCategoryPage.vue"),
},
{
path: ":categorySlug/:contentSlug",
props: true,
component: () =>
import("@/pages/mediaLibrary/MediaLibraryContentPage.vue"),
},
],
},
{
path: ":categorySlug",
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryCategoryPage.vue"),
path: "competence",
component: () => import("@/pages/competence/CompetenceParentPage.vue"),
children: [
{
path: "",
props: true,
name: COMPETENCE_ROUTE,
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
},
{
path: "certificates",
name: CERTIFICATES_ROUTE,
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
},
{
path: "certificates/:certificateSlug",
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
name: SELF_EVALUATION_ROUTE,
path: "self-evaluation-and-feedback",
props: true,
component: () =>
import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"),
},
{
path: "competences",
name: COMPETENCES_ROUTE,
props: true,
component: () =>
import("@/pages/competence/ActionCompetenceListPage.vue"),
},
],
},
{
path: ":categorySlug/:contentSlug",
path: "learn",
children: [
{
path: "",
props: true,
component: () =>
import("../pages/learningPath/learningPathPage/LearningPathPage.vue"),
},
{
path: ":circleSlug",
component: () =>
import("../pages/learningPath/circlePage/CirclePage.vue"),
props: true,
},
{
path: ":circleSlug/evaluate/:learningUnitSlug",
component: () =>
import(
"../pages/learningPath/selfEvaluationPage/SelfEvaluationPage.vue"
),
props: true,
},
{
path: ":circleSlug/:contentSlug",
component: () =>
import(
"../pages/learningPath/learningContentPage/LearningContentPage.vue"
),
props: true,
},
],
},
{
path: "profile/:userId",
component: () => import("@/pages/userProfile/UserProfilePage.vue"),
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryContentPage.vue"),
children: [
{
path: "learning-path",
component: () =>
import("@/pages/userProfile/LearningPathProfilePage.vue"),
props: true,
name: "profileLearningPath",
meta: {
hideChrome: true,
showCloseButton: true,
},
},
{
path: "competence",
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
props: true,
name: "profileCompetence",
meta: {
hideChrome: true,
showCloseButton: true,
},
children: [
{
path: "",
name: "competenceMain",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue"
),
},
{
path: "evaluations",
name: "competenceEvaluations",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue"
),
},
{
path: "certificates/:certificateSlug",
name: "competenceCertificateDetail",
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
path: "certificates",
name: "competenceCertificates",
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
},
],
},
],
},
{
path: "learning-mentor",
component: () => import("@/pages/learningMentor/mentor/MentorIndexPage.vue"),
name: "learningMentor",
children: [
{
path: "",
component: () =>
import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"),
name: "mentorsAndParticipants",
},
{
path: "tasks",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "self-evaluation-feedback/:learningUnitId",
component: () =>
import("@/pages/learningMentor/mentor/SelfEvaluationFeedbackPage.vue"),
name: "mentorSelfEvaluationFeedback",
props: true,
},
{
path: "details",
component: () =>
import("@/pages/learningMentor/mentor/MentorDetailParentPage.vue"),
children: [
{
path: "praxis-assignments/:praxisAssignmentId",
component: () =>
import(
"@/pages/learningMentor/mentor/MentorPraxisAssignmentPage.vue"
),
name: "learningMentorPraxisAssignments",
props: true,
},
{
path: "self-evaluation-feedback-assignments/:learningUnitId",
component: () =>
import(
"@/pages/learningMentor/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
),
name: "learningMentorSelfEvaluationFeedbackAssignments",
props: true,
},
],
},
],
},
{
path: "assignment-evaluation/:assignmentId/:userId",
component: () =>
import("@/pages/assignmentEvaluation/AssignmentEvaluationPage.vue"),
props: true,
},
{
path: "cockpit",
name: "cockpit",
component: () =>
import("@/pages/cockpit/cockpitPage/CockpitExpertParentPage.vue"),
props: true,
children: [
{
path: "",
component: () =>
import("@/pages/cockpit/cockpitPage/CockpitExpertHomePage.vue"),
name: COCKPIT_ROUTE,
props: true,
},
{
path: "profile/:userId/:circleSlug",
component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"),
props: true,
},
{
path: "feedback/:circleId",
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
props: true,
},
{
path: "assignment/:assignmentId",
component: () =>
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
props: true,
},
{
path: "attendance",
component: () =>
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
props: true,
name: ATTENDANCE_ROUTE,
},
{
path: "documents",
component: () => import("@/pages/cockpit/documentPage/DocumentPage.vue"),
props: true,
name: DOCUMENTS_ROUTE,
},
],
},
],
},
{
path: "/course/:courseSlug/competence",
props: true,
component: () => import("@/pages/competence/CompetenceParentPage.vue"),
children: [
{
path: "",
props: true,
name: COMPETENCE_ROUTE,
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
},
{
path: "certificates",
name: CERTIFICATES_ROUTE,
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
},
{
path: "certificates/:certificateSlug",
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
name: SELF_EVALUATION_ROUTE,
path: "self-evaluation-and-feedback",
props: true,
component: () =>
import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"),
},
{
path: "competences",
name: COMPETENCES_ROUTE,
props: true,
component: () => import("@/pages/competence/ActionCompetenceListPage.vue"),
},
],
},
{
path: "/course/:courseSlug/learn",
component: () =>
import("../pages/learningPath/learningPathPage/LearningPathPage.vue"),
props: true,
},
{
path: "/course/:courseSlug/learn/:circleSlug",
component: () => import("../pages/learningPath/circlePage/CirclePage.vue"),
props: true,
},
{
path: "/course/:courseSlug/learn/:circleSlug/evaluate/:learningUnitSlug",
component: () =>
import("../pages/learningPath/selfEvaluationPage/SelfEvaluationPage.vue"),
props: true,
},
{
path: "/course/:courseSlug/learn/:circleSlug/:contentSlug",
component: () =>
import("../pages/learningPath/learningContentPage/LearningContentPage.vue"),
props: true,
},
{
path: "/lernbegleitung/:courseId/invitation/:invitationId",
component: () => import("@/pages/learningMentor/InvitationAcceptPage.vue"),
@ -178,158 +363,6 @@ const router = createRouter({
public: true,
},
},
{
path: "/course/:courseSlug/profile/:userId",
component: () => import("@/pages/userProfile/UserProfilePage.vue"),
props: true,
children: [
{
path: "learning-path",
component: () => import("@/pages/userProfile/LearningPathProfilePage.vue"),
props: true,
name: "profileLearningPath",
meta: {
hideChrome: true,
showCloseButton: true,
},
},
{
path: "competence",
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
props: true,
name: "profileCompetence",
meta: {
hideChrome: true,
showCloseButton: true,
},
children: [
{
path: "",
name: "competenceMain",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue"
),
},
{
path: "evaluations",
name: "competenceEvaluations",
component: () =>
import(
"@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue"
),
},
{
path: "certificates/:certificateSlug",
name: "competenceCertificateDetail",
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
path: "certificates",
name: "competenceCertificates",
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
},
],
},
],
},
{
path: "/course/:courseSlug/learning-mentor",
component: () => import("@/pages/learningMentor/mentor/MentorIndexPage.vue"),
props: true,
name: "learningMentor",
children: [
{
path: "",
component: () =>
import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"),
name: "mentorsAndParticipants",
},
{
path: "tasks",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "self-evaluation-feedback/:learningUnitId",
component: () =>
import("@/pages/learningMentor/mentor/SelfEvaluationFeedbackPage.vue"),
name: "mentorSelfEvaluationFeedback",
props: true,
},
{
path: "details",
component: () =>
import("@/pages/learningMentor/mentor/MentorDetailParentPage.vue"),
children: [
{
path: "praxis-assignments/:praxisAssignmentId",
component: () =>
import("@/pages/learningMentor/mentor/MentorPraxisAssignmentPage.vue"),
name: "learningMentorPraxisAssignments",
props: true,
},
{
path: "self-evaluation-feedback-assignments/:learningUnitId",
component: () =>
import(
"@/pages/learningMentor/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
),
name: "learningMentorSelfEvaluationFeedbackAssignments",
props: true,
},
],
},
],
},
{
path: "/course/:courseSlug/assignment-evaluation/:assignmentId/:userId",
component: () =>
import("@/pages/assignmentEvaluation/AssignmentEvaluationPage.vue"),
props: true,
},
{
path: "/course/:courseSlug/cockpit",
name: "cockpit",
children: [
{
path: "",
component: () => import("@/pages/cockpit/cockpitPage/CockpitExpertPage.vue"),
props: true,
},
{
path: "profile/:userId/:circleSlug",
component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"),
props: true,
},
{
path: "feedback/:circleId",
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
props: true,
},
{
path: "assignment/:assignmentId",
component: () =>
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
props: true,
},
{
path: "attendance",
component: () =>
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
props: true,
},
{
path: "documents",
component: () => import("@/pages/cockpit/documentPage/DocumentPage.vue"),
props: true,
},
],
},
{
path: "/statistic/:courseSlug",
props: true,
@ -399,7 +432,7 @@ const router = createRouter({
{
path: "/profile",
component: () => import("@/pages/personalProfile/PersonalProfilePage.vue"),
name: "personalProfile",
name: PERSONAL_PROFILE_ROUTE,
},
{
path: "/settings",

View File

@ -3,3 +3,7 @@ export const CERTIFICATES_ROUTE = "certificates";
export const SELF_EVALUATION_ROUTE = "selfEvaluationAndFeedback";
export const COMPETENCES_ROUTE = "competences";
export const SETTINGS_ROUTE = "settings";
export const COCKPIT_ROUTE = "cockpit-home";
export const ATTENDANCE_ROUTE = "attendance";
export const DOCUMENTS_ROUTE = "documents";
export const PERSONAL_PROFILE_ROUTE = "personalProfile";

View File

@ -1,50 +1,14 @@
import { useCourseData } from "@/composables";
import { useCourseData, useCourseSessionDetailQuery } from "@/composables";
import { useUserStore } from "@/stores/user";
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
type CircleExpertCockpit = CircleLight & {
name: string;
};
export type ExpertCockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined;
circles: CircleExpertCockpit[] | undefined;
currentCircle: CircleExpertCockpit | undefined;
};
export const useExpertCockpitStore = defineStore({
id: "expertCockpit",
state: () => {
return {
courseSessionMembers: undefined,
circles: [],
currentCircle: undefined,
} as ExpertCockpitStoreState;
},
actions: {
async loadCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug);
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
if (this.circles?.length) {
await this.setCurrentCourseCircle(this.circles[0].slug);
}
},
async setCurrentCourseCircle(circleSlug: string) {
this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
},
async setCurrentCourseCircleFromEvent(event: CircleLight) {
await this.setCurrentCourseCircle(event.slug);
},
},
});
async function courseCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
@ -74,3 +38,51 @@ async function courseCircles(
return [];
}
export const useExpertCockpitStore = defineStore("expertCockpit", () => {
const courseSessionMembers = ref<CourseSessionUser[] | undefined>(undefined);
const circles = ref<CircleExpertCockpit[] | undefined>([]);
const currentCircle = ref<CircleExpertCockpit | undefined>(undefined);
const courseSessionDetailResult = useCourseSessionDetailQuery();
const attendanceCourses = computed(() => {
return (
courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? []
);
});
const currentCourse = computed(() => {
return attendanceCourses.value.find(
(i) => i.learning_content.circle?.id == currentCircle.value?.id
);
});
const loadCircles = async (
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) => {
log.debug("loadCircles called", courseSlug);
circles.value = await courseCircles(courseSlug, currentCourseSessionUser);
if (circles.value?.length) {
await setCurrentCourseCircle(circles.value[0].slug);
}
};
const setCurrentCourseCircle = async (circleSlug: string) => {
currentCircle.value = circles.value?.find((c) => c.slug === circleSlug);
};
const setCurrentCourseCircleFromEvent = async (event: CircleLight) => {
await setCurrentCourseCircle(event.slug);
};
return {
courseSessionMembers,
circles,
currentCircle,
loadCircles,
currentCourse,
setCurrentCourseCircleFromEvent,
};
});

View File

@ -0,0 +1,13 @@
import { isInFuture } from "@/components/dueDates/dueDatesUtils";
export type Status = "done" | "soon" | "now";
export const getStatus = (done: boolean, date: string): Status => {
if (done) {
return "done";
}
if (isInFuture(date)) {
return "soon";
}
return "now";
};

View File

@ -1,20 +1,24 @@
import {EXPERT_COCKPIT_URL, login} from "./helpers";
import { EXPERT_COCKPIT_URL, login } from "./helpers";
describe("settings.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
cy.intercept("/server/graphql").as("graphql");
});
describe("with circle documents enabled", () => {
it("student can see circle documents", () => {
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug");
cy.wait(["@graphql", "@graphql"]);
cy.get('[data-cy="circle-document-section"]').should("exist");
});
it("trainer can see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit(EXPERT_COCKPIT_URL);
cy.wait(["@graphql", "@graphql"]);
cy.get('[data-cy="circle-documents"]').should("exist");
});
});
@ -27,6 +31,7 @@ describe("settings.cy.js", () => {
it("student cannot see circle documents", () => {
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug");
cy.wait(["@graphql", "@graphql"]);
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get('[data-cy="circle-document-section"]').should("not.exist");
});
@ -34,6 +39,7 @@ describe("settings.cy.js", () => {
it("trainer cannot see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit(EXPERT_COCKPIT_URL);
cy.wait(["@graphql", "@graphql"]);
cy.get('[data-cy="circle-documents"]').should("not.exist");
});
});