Compare commits

...

114 Commits

Author SHA1 Message Date
Ramon Wenger 34efe68842 Work in progress 2024-12-03 17:45:42 +01:00
Ramon Wenger cc2ae195c0 Refactor Certificate component, add new page and component 2024-12-03 17:44:02 +01:00
Ramon Wenger 2c4512ba91 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
2024-11-27 14:14:05 +00:00
Ramon Wenger f9d952a5e8 Fix heading width on document page 2024-11-27 12:11:05 +01:00
Christian Cueni 9692c71fbc Merged in feature/VBV-772-automatic-ab-bb-assignment-model (pull request #426)
Noch nicht mergen: Feature/VBV-772 automatic ab bb assignment model

Approved-by: Daniel Egger
2024-11-25 08:14:00 +00:00
Christian Cueni 1a909431cf Add comment 2024-11-25 08:03:41 +01:00
Ramon Wenger cf13030f24 Adjust width and padding for the circle dropdown used as heading 2024-11-21 15:54:39 +01:00
Ramon Wenger bc5055324c Fix icon color on hover 2024-11-21 15:10:00 +01:00
Ramon Wenger a620803413 Switch between button or link on attendance overview conditionally 2024-11-21 15:03:07 +01:00
Ramon Wenger aa7137b764 Fix minor styling issues 2024-11-21 15:02:26 +01:00
Daniel Egger e87ab1da57 Move `delete` code to signal 2024-11-21 14:27:06 +01:00
Ramon Wenger b785e327c6 Fix test setup to prevent warning 2024-11-20 20:17:01 +01:00
Ramon Wenger c68057448e Add translations, clean up code 2024-11-20 20:09:44 +01:00
Christian Cueni 68938ba44b Get or create organisation 2024-11-20 15:20:09 +01:00
Christian Cueni 6ca8469455 Add tests, refactor services 2024-11-20 13:20:45 +01:00
Christian Cueni 5e84490703 Add org supervisor model and services 2024-11-20 09:21:07 +01:00
Ramon Wenger 82f086023c Switch heading with dropdown on documents page, remove back link 2024-11-19 16:52:49 +01:00
Ramon Wenger 3b8bc42cbe Re-add missing cypress data property 2024-11-19 16:52:39 +01:00
Ramon Wenger 71e2b7cef3 Update styling on attendance check page 2024-11-19 16:52:16 +01:00
Ramon Wenger 0536b7a537 Remove old confirmation text on presence check page 2024-11-19 16:52:01 +01:00
Ramon Wenger 4d367ff40d Fix attendance overview layout on mobile 2024-11-19 16:51:47 +01:00
Ramon Wenger 1d5bd24571 Use french quotation marks 2024-11-19 16:51:33 +01:00
Ramon Wenger 31db223661 Fix styling of mobile menu 2024-11-19 16:51:26 +01:00
Ramon Wenger 8631fca09d Update spacing in top navigation 2024-11-19 16:51:19 +01:00
Ramon Wenger 72f7a7f68c Fix profile sidebar on mobile 2024-11-19 16:50:59 +01:00
Ramon Wenger 2386b5207e Fix course session select button 2024-11-19 16:50:45 +01:00
Ramon Wenger 4bf47efa2d Fix font size in dropdown 2024-11-19 16:50:35 +01:00
Ramon Wenger 3d742fb03a Add translations 2024-11-14 10:38:05 +01:00
Ramon Wenger 995dbb5c78 Pass correct props to AttendanceStatus on the overview page 2024-11-14 10:37:28 +01:00
Ramon Wenger 2fc37d0d0b Clean up code 2024-11-14 10:37:28 +01:00
Ramon Wenger f8b8347bb9 Refactor attendance page, add components for the check and the status 2024-11-14 10:37:24 +01:00
Ramon Wenger abe60e7468 Make dropdown component usable as heading 2024-11-14 10:34:10 +01:00
Ramon Wenger eb1d814680 Add test for new component and date helpers 2024-11-14 10:34:10 +01:00
Ramon Wenger 1c669474e9 Extend AttendanceStatus component with date functionality 2024-11-14 10:34:10 +01:00
Ramon Wenger d6c182b5ab Fix more route props 2024-11-14 10:34:10 +01:00
Ramon Wenger 93660cf421 Hide documents link conditionally 2024-11-14 10:34:10 +01:00
Ramon Wenger 55515dd10f Fix route props 2024-11-14 10:34:10 +01:00
Ramon Wenger db399e2aa2 Add attendance widget and status component 2024-11-14 10:34:10 +01:00
Ramon Wenger 3f5d837db9 Add subpages and subnavigation to cockpit page 2024-11-14 10:34:10 +01:00
Ramon Wenger bc2dd2ba6e Fix styling issue 2024-11-14 10:34:10 +01:00
Ramon Wenger 20e2a2b172 Clean up router index, add route names 2024-11-14 10:34:10 +01:00
Ramon Wenger d361dabd16 Merged in feature/new-navigation-layout (pull request #414)
Feature/new navigation layout

Approved-by: Elia Bieri
2024-11-14 08:57:21 +00:00
Ramon Wenger 028ff05020 Add missing settings link
Resolves VBV-442 #complete
2024-11-13 17:55:05 +01:00
Ramon Wenger 3faf20ac07 Fix some issues mentioned in PR review 2024-11-13 17:55:05 +01:00
Ramon Wenger 9b89177db5 Move course session switcher to top navigation 2024-11-13 17:55:05 +01:00
Ramon Wenger f560531df2 Split up menu into internal and external links 2024-11-13 17:55:04 +01:00
Ramon Wenger 31bbf046fa Update styling of mobile menu 2024-11-13 17:55:04 +01:00
Ramon Wenger 922bb9da32 Add missing import 2024-11-13 17:55:04 +01:00
Ramon Wenger 644a107377 Re-add cypress data attribute 2024-11-13 17:55:04 +01:00
Ramon Wenger 44e618bf9d Move SubNavigation to its own component 2024-11-13 17:55:04 +01:00
Ramon Wenger 72c9fe8c9e Update mobile menu 2024-11-13 17:55:04 +01:00
Ramon Wenger 228bfd9c27 Show selected course session on mobile 2024-11-13 17:55:04 +01:00
Ramon Wenger e5cbdc1620 Add styling to active mobile navigation item 2024-11-13 17:55:04 +01:00
Ramon Wenger 0c1a2b3151 Update mobile subnavigation to indicate the active item 2024-11-13 17:55:04 +01:00
Ramon Wenger 6c6bbb9e22 Simplify Subnavigation and create component to handle external vs
internal links
2024-11-13 17:55:04 +01:00
Ramon Wenger 28027eb96f Move selected course session indicator 2024-11-13 17:55:04 +01:00
Ramon Wenger 69efd0de2d Add navigation button to go back to the Dashboard 2024-11-13 17:55:04 +01:00
Ramon Wenger f2a422157f Move mobile button 2024-11-13 17:55:04 +01:00
Ramon Wenger 7a782ed911 Start refactoring the subnavigation on the CompetenceParentPage 2024-11-13 17:55:04 +01:00
Elia Bieri 485ce827b4 Merged in bugfix/fix-HMR-with-gql-schema-gen (pull request #424)
Ignore minified GQL schema in vite watch
2024-11-13 14:45:11 +00:00
Elia Bieri c9052b9691 Add remark about limitations of approach 2024-11-13 15:23:25 +01:00
Christian Cueni bc55b1b660 Merged in bugfix/VBV-769-expert-feedback-view (pull request #419)
Fix index, check if expert is in session and circle

Approved-by: Elia Bieri
2024-11-06 14:55:55 +00:00
Christian Cueni 78ba5cc37f Merged develop into bugfix/VBV-769-expert-feedback-view 2024-11-06 14:18:54 +00:00
Elia Bieri 7b1db672ab Ignore minified GQL schema in vite watch 2024-11-06 14:39:26 +01:00
Christian Cueni 1e65c557c1 Change if evaluation order 2024-11-06 14:33:56 +01:00
Ramon Wenger d3f1c2adfa Merged in feature/navigation-missing-import (pull request #423)
Add missing import

Approved-by: Elia Bieri
2024-11-06 13:28:33 +00:00
Elia Bieri bf810b3aaa Fix z-index of popover content 2024-11-06 14:16:24 +01:00
Ramon Wenger f6e3d1c69a Add missing import 2024-11-06 11:39:18 +01:00
Christian Cueni 62a7476d8a Merged in feature/VBV-783-cembra-agb (pull request #422)
Add cembra Tos and privacy hint
2024-11-06 09:55:55 +00:00
Christian Cueni 09196007e6 Merged develop into feature/VBV-783-cembra-agb 2024-11-06 06:32:11 +00:00
Elia Bieri a91a0f096c Merged in bugfix/VBV-760-responsiveness-verbessern (pull request #410)
VBV-760 Responsiveness verbessern

Approved-by: Stéphanie Rotzetter
2024-11-05 16:11:02 +00:00
Christian Cueni 302d154f41 Add cembra Tos and privacy hint 2024-11-05 07:13:07 +01:00
Christian Cueni c99fd7b17a Merged in feature/VBV-774-course-autogewerbe (pull request #418)
Feature/VBV-774 course autogewerbe

Approved-by: Daniel Egger
2024-11-04 08:24:23 +00:00
Christian Cueni 94cafb9b73 Merged develop into feature/VBV-774-course-autogewerbe 2024-10-31 13:44:06 +00:00
Christian Cueni 9160084f98 Merged in feature/VBV-706-sso-error-sync (pull request #421)
Add sso error sync job
2024-10-31 10:46:17 +00:00
Christian Cueni 1d0ee7b906 Add sso error sync job 2024-10-31 11:17:17 +01:00
Christian Cueni 014ecc31c4 Merged in feature/VBV-781-fix-content-disposition-for-files-with-comma (pull request #420)
Fix double content-disposition header for files with commas
2024-10-31 09:39:36 +00:00
Christian Cueni 5d40bce806 Fix double content-disposition header for files with commas 2024-10-31 10:20:42 +01:00
Christian Cueni 8c8b11b354 Fix index, check if expert is in session and circle 2024-10-30 13:25:40 +01:00
Christian Cueni 908f3cade9 Merged develop into feature/VBV-774-course-autogewerbe 2024-10-30 08:04:38 +00:00
Christian Cueni 67a19b8513 Add new test circle, add tests 2024-10-30 07:14:58 +01:00
Ramon Wenger 345e935655 Merged in feature/new-navigation-structure (pull request #407)
Feature/new navigation structure

Approved-by: Elia Bieri
2024-10-29 15:35:06 +00:00
Elia Bieri e55a5e9e42 Rework 2024-10-29 15:51:07 +01:00
Elia Bieri d4680157ac Merged in bugfix/VBV-715-falscher-link-auf-praxisbildner-dashboard (pull request #411)
VBV-715 Falscher Link auf Praxisbildner Dashboard

Approved-by: Christian Cueni
2024-10-29 08:08:20 +00:00
Elia Bieri e277268426 Fix python UT 2024-10-28 14:36:58 +00:00
Elia Bieri 564d45341c Unify praxisbildner dashboard with berufsbildner 2024-10-28 14:36:58 +00:00
Daniel Egger 2e6c305cea Merged in feature/feature-deploy-readme (pull request #416)
Improve docs and deploy script
2024-10-28 13:26:47 +00:00
Daniel Egger f24d2ca01e Improve docs and deploy script 2024-10-28 14:10:06 +01:00
Christian Cueni ea1d140fc1 Add serializer 2024-10-28 12:50:52 +01:00
Christian Cueni 1d2aa38896 Add new learning content type 2024-10-28 11:52:24 +01:00
Christian Cueni 5850321776 Hide contact box if learning_mentor is disabled 2024-10-28 10:39:16 +01:00
Christian Cueni 5d933cd0ec Merged in feature/VBV-778-bb-content-access (pull request #415)
Allow learning content for Berufsbildner
2024-10-28 06:53:56 +00:00
Christian Cueni 944dae9a07 Merged develop into feature/VBV-778-bb-content-access 2024-10-28 05:56:52 +00:00
Christian Cueni fb26dda971 Allow learning content for Berufsbildner 2024-10-26 17:01:58 +02:00
Christian Cueni 39133d2d28 Merged in fix/logout (pull request #413)
Fix error if uri is not correctly set, use ruff
2024-10-24 18:04:28 +00:00
Christian Cueni 5320694172 Merged develop into fix/logout 2024-10-24 13:46:42 +00:00
Christian Cueni aca3e2a1c4 Fix error if uri is not correctly set, use ruff 2024-10-24 14:32:45 +02:00
Christian Cueni c11e4d1105 Merged in feature/VBV-773-new-roles (pull request #412)
Add new roles
2024-10-24 12:23:13 +00:00
Christian Cueni 925d737ee4 Merged develop into feature/VBV-773-new-roles 2024-10-24 11:47:03 +02:00
Christian Cueni 1f69deaa7f Merged in feature/VBV-768-doc (pull request #408)
Add minimal test user info
2024-10-24 09:40:15 +00:00
Christian Cueni 09750abbc2 Add new roles 2024-10-24 11:39:03 +02:00
Christian Cueni 82f9d66903 Merged develop into feature/VBV-768-doc 2024-10-23 13:44:07 +00:00
Christian Cueni 50a0927a23 Merged in feature/VBV-753-sso-logout (pull request #409)
Add logout with id_token_hint

Approved-by: Elia Bieri
2024-10-23 13:43:50 +00:00
Elia Bieri ffe1709e7f Make Lernbegleitung and Student responsive 2024-10-23 12:02:51 +02:00
Christian Cueni ac07a3e253 Merged develop into feature/VBV-753-sso-logout 2024-10-23 09:26:35 +00:00
Elia Bieri dc572ad1a3 Merged in feature/VBV-751-kaufprozess-validierung-in-plz (pull request #406)
PLZ validieren im Onboarding

Approved-by: Christian Cueni
2024-10-22 13:54:51 +00:00
Elia Bieri 4e3ece6f72 Fix ausbildungsverantwortlicher dashboard 2024-10-22 13:54:59 +02:00
Elia Bieri 37bac32999 Make trainer dashboard and cockpit responsive 2024-10-22 13:40:29 +02:00
Christian Cueni 9e53b8814b Add logout with id_token_hint 2024-10-22 10:44:27 +02:00
Elia Bieri 6902521736 Make regex more explicit 2024-10-22 10:33:06 +02:00
Elia Bieri d09b6cce04 Fix postcode validation regex 2024-10-22 09:29:49 +02:00
Christian Cueni fd763df546 Add minimal test user info 2024-10-21 11:37:10 +02:00
Elia Bieri be1df5ce1a Format translations 2024-10-17 15:44:16 +02:00
Elia Bieri 9be46400b0 Validate postalcodes in onboarding 2024-10-17 15:39:27 +02:00
138 changed files with 3230 additions and 1175 deletions

3
.gitignore vendored
View File

@ -285,3 +285,6 @@ git-crypt-encrypted-files-check.txt
/client/src/gql/dist/minifiedSchema.json
/sftptest/
# BabelEdit translation software
/client/bable_edit.babel

View File

@ -181,15 +181,23 @@ Deployment happens manually via Bitbucket Pipelines
Bitbucket Pipelines name: myvbv
https://myvbv.control.iterativ.ch/
https://myvbv.iterativ.ch/
https://myvbv.iterativ.ch/v
Deployment happens manually via Bitbucket Pipelines
### CapRover feature branch deployment
You can deploy every feature branch to CapRover directly from Bitbucket Pipelines
with a manual step.
with a manual step. Either in the "normal" pipeline by running "deploy feature"
as last manual step.
You can also select deploy-step direclty under
`Branches > Actions > Run pipeline` for a branch
and then `custom: deploy-feature-branch -> Run` in the modal.
![](./docs/assets/deploy-feature1.png)
![](./docs/assets/deploy-feature2.png)
When you run caprover_deploy.sh without arguments, it will deploy the current branch
@ -199,6 +207,9 @@ When you run caprover_deploy.sh without arguments, it will deploy the current br
### Cleanup caprover feature branch deployments
You can delete the CapRover App and DB directly in the CapRover interface
or use the `caprover_cleanup.py` script.
```bash
# by default it will delete all vbv-feature-* apps
python caprover_cleanup.py
@ -295,7 +306,25 @@ graphql schema.
- What about the generated types from `codegen`? Hand written types seem to be better.
- The functions in `cacheExchange` should be nearer the concrete implementation...
## Load prod data for testing
## Test users
### Local development
If `prepare_server.sh` was run, the following users are available (password: `test` and found in `create_default_users.py`):
- Trainer
- test-trainer1@example.com
- test-trainer2@example.com
- Teilnehmer
- test-student1@example.com
- test-student2@example.com
- test-student3@example.com
- Regionenleiter
- test-supervisor1@example.com
- Berufsbildner
- test-berufsbildner1@example.com
- Ausbildungsverantwortlicher
- test-ausbildungsverantwortlicher1@example.com
### Load prod data for local testing
1. Checkout the [vbv-devops](https://bitbucket.org/iterativ/iterativ-devops/src/master/) repository
2. Change into the `backups` directory
@ -306,3 +335,6 @@ graphql schema.
for csu in CourseSessionUser.objects.all():
csu.user.set_password("test")
```
### Test users on production
On production there are CourseSessions in the VV and üK courses with test users. The data can be rested via the admin
interface ("Iterativ Testdurchführungen zurücksetzen"). All users use local login and can be found in 1password.

View File

@ -64,6 +64,11 @@ def main(app_name, image_name, environment_file):
default_allowed_hosts = f"{app_name}.iterativ.ch,{app_name}.control.iterativ.ch"
app_environment = env.str("IT_APP_ENVIRONMENT", "dev-feature")
if app_environment == "local":
# local is never the correct value...
app_environment = "dev-feature"
cap.create_and_update_app(
app_name=app_name,
enable_ssl=True,
@ -72,7 +77,7 @@ def main(app_name, image_name, environment_file):
image_name=image_name,
container_http_port=7555,
environment_variables={
"IT_APP_ENVIRONMENT": env.str("IT_APP_ENVIRONMENT", "dev-feature"),
"IT_APP_ENVIRONMENT": app_environment,
"IT_DEFAULT_ADMIN_PASSWORD": env.str(
"IT_DEFAULT_ADMIN_PASSWORD", "ACEEs0DCmNaPxdoNV8vhccuCTRl9b"
),

View File

@ -36,8 +36,8 @@ async function changeLocale(language: AvailableLanguages) {
</a>
</div>
<router-link
class="lg:ml-8"
v-if="isVVLearningMentor(courseSessionsStore.currentCourseSession)"
class="lg:ml-8"
:to="vvBuyLink.href.value"
data-cy="buy-vv-link"
>

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

@ -55,6 +55,7 @@ const submittables = computed(() => {
lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV" ||
lc.content_type === "learnpath.LearningContentFeedbackAutomobilGewerbe" ||
lc.content_type === "learnpath.LearningContentEdoniqTest"
);
@ -76,7 +77,8 @@ const submittables = computed(() => {
const isFeedback = (lc: LearningContent) => {
return (
lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV"
lc.content_type === "learnpath.LearningContentFeedbackVV" ||
lc.content_type === "learnpath.LearningContentFeedbackAutomobilGewerbe"
);
};

View File

@ -29,7 +29,7 @@ onMounted(async () => {
</script>
<template>
<div class="w-[325px]">
<div class="w-60 sm:w-[325px]">
<BaseBox
:details-link="`/dashboard/persons?course=${props.courseId}`"
data-cy="dashboard.mentor.menteeCount"
@ -37,14 +37,16 @@ onMounted(async () => {
>
<template #title>{{ $t("a.Personen") }}</template>
<template #content>
<div :class="['flex flex-row space-x-3 bg-white', slim ? '' : 'pb-6']">
<div
:class="['flex flex-row flex-wrap items-center bg-white', slim ? '' : 'pb-6']"
>
<div
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
data-cy="dashboard.mentor.menteeCountValue"
>
<span>{{ menteeCount }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">
<p class="mt-0">
{{ $t("a.Personen, die du begleitest") }}
</p>
</div>

View File

@ -31,7 +31,7 @@ onMounted(async () => {
<template>
<div v-if="assignment">
<div class="w-[395px]">
<div class="sm:w-[395px]">
<AssignmentProgressSummaryBox
:total-assignments="assignment.total_count"
:achieved-points-count="assignment.points_achieved_count"

View File

@ -24,7 +24,7 @@ const data = computed(() => ({
</script>
w
<template>
<div class="flex flex-row items-center space-x-8">
<div class="flex flex-row flex-wrap items-center gap-y-4 space-x-8 sm:gap-y-0">
<div class="size-32">
<Pie
:data="data"
@ -39,7 +39,7 @@ w
}"
/>
</div>
<div class="flex flex-col space-y-2">
<div class="flex flex-col items-start">
<div
v-for="(value, key) in props.data"
:key="key"

View File

@ -25,7 +25,7 @@ onMounted(async () => {
</script>
<template>
<div v-if="competence" class="w-[395px]">
<div v-if="competence" class="sm:w-[395px]">
<CompetenceSummaryBox
:fail-count="competence.fail_count"
:success-count="competence.success_count"

View File

@ -9,6 +9,7 @@ import UkStatistics from "@/components/dashboard/UkStatistics.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import type { DashboardCourseConfigType, WidgetType } from "@/services/dashboard";
import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed } from "vue";
import TrainingResponsibleStatistics from "./TrainingResponsibleStatistics.vue";
@ -23,6 +24,9 @@ const props = defineProps<{
courseConfig: DashboardCourseConfigType | undefined;
}>();
const breakpoints = useBreakpoints(breakpointsTailwind);
const smAndLarger = breakpoints.greaterOrEqual("sm");
const courseSlug = computed(() => props.courseConfig?.course_slug ?? "");
const courseName = computed(() => props.courseConfig?.course_title ?? "");
const numberOfMentorWidgets = computed(() => {
@ -65,6 +69,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
cyKey: "lm-dashboard-link",
};
}
if (props.courseConfig?.role_key === "MentorUK") {
return {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "a.Teilnehmer Vorschau",
cyKey: "attendance-dashboard-link",
};
}
return {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "Weiter lernen",
@ -75,7 +86,6 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
function hasActionButton(): boolean {
return (
props.courseConfig?.role_key !== "MentorUK" &&
props.courseConfig?.role_key !== "Ausbildungsverantwortlicher" &&
props.courseConfig?.role_key !== "Berufsbildner"
);
@ -90,8 +100,10 @@ function hasActionButton(): boolean {
>
<div class="flex flex-col space-y-8 bg-white p-6">
<div class="border-b border-gray-300 pb-8">
<div class="flex flex-row items-start justify-between">
<h3 class="mb-4 text-3xl" data-cy="db-course-title">{{ courseName }}</h3>
<div class="flex flex-row flex-wrap items-start justify-between pb-3 sm:pb-0">
<h3 class="mb-4 text-xl sm:text-3xl" data-cy="db-course-title">
{{ courseName }}
</h3>
<a
v-if="hasActionButton()"
:href="actionButtonProps.href"
@ -108,7 +120,7 @@ function hasActionButton(): boolean {
<router-link
v-if="courseConfig.has_preview"
:to="getLearningPathUrl(courseConfig.course_slug)"
class="inline-block pl-6"
class="inline-block pt-3 sm:pl-6 sm:pt-0"
target="_blank"
>
<div class="flex items-center">
@ -131,7 +143,7 @@ function hasActionButton(): boolean {
:key="courseSlug"
:course-slug="courseSlug"
:course-session-id="courseConfig.session_to_continue_id"
diagram-type="horizontal"
:diagram-type="smAndLarger ? 'horizontal' : 'horizontalSmall'"
></LearningPathDiagram>
</div>
@ -173,7 +185,7 @@ function hasActionButton(): boolean {
<div
v-if="numberOfMentorWidgets > 0"
class="flex flex-col flex-wrap items-stretch md:flex-row"
class="flex flex-col flex-wrap items-stretch space-y-4 sm:space-y-0 md:flex-row"
>
<AgentConnectionCount
v-if="hasWidget('MentorPersonWidget')"

View File

@ -18,6 +18,8 @@ const summary = computed(() => {
return mentorAssignmentData.value?.assignments ?? null;
});
const courseSlug = computed(() => mentorAssignmentData.value?.course_slug);
onMounted(async () => {
mentorAssignmentData.value = await fetchMentorCompetenceSummary(
props.courseId,
@ -27,9 +29,9 @@ onMounted(async () => {
</script>
<template>
<div v-if="summary" class="w-[325px]">
<div v-if="summary" class="w-60 sm:w-[325px]">
<BaseBox
:details-link="`/dashboard/persons-competence?course=${props.courseId}`"
:details-link="`/statistic/${props.agentRole}/${courseSlug}/assignment`"
data-cy="dashboard.mentor.competenceSummary"
>
<template #title>{{ $t("Kompetenznachweise") }}</template>

View File

@ -18,14 +18,14 @@ onMounted(async () => {
</script>
<template>
<div class="w-[325px]">
<div class="w-60 sm:w-[325px]">
<BaseBox
:details-link="`/course/${props.courseSlug}/learning-mentor/tasks`"
data-cy="dashboard.mentor.openTasksCount"
>
<template #title>{{ $t("a.Zu erledigen") }}</template>
<template #content>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div class="flex flex-row flex-wrap space-x-3 bg-white pb-6">
<div
class="flex h-[74px] w-[74px] items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-3xl font-bold"
>

View File

@ -54,7 +54,7 @@ const attendanceCountPerChosenProfile = computed(() => {
<template>
<div class="flex flex-col">
<div class="flex flex-col flex-wrap items-stretch md:flex-row">
<div class="flex flex-col flex-wrap items-stretch gap-y-4 sm:gap-y-0 md:flex-row">
<BaseBox
:details-link="`/dashboard/cost/${courseSessionId}`"
data-cy="dashboard.stats.trainingResponsible.cost"

View File

@ -64,7 +64,7 @@ onMounted(async () => {
<template>
<div v-if="statistics" class="space-y-8">
<div
class="flex flex-col flex-wrap justify-between gap-x-5 border-b border-gray-300 pb-8 last:border-0 md:flex-row"
class="flex flex-col flex-wrap justify-between gap-x-5 gap-y-5 border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<AttendanceSummaryBox
class="flex-grow"
@ -80,7 +80,7 @@ onMounted(async () => {
/>
</div>
<div
class="flex flex-col flex-wrap gap-x-5 border-b border-gray-300 align-top last:border-0 md:flex-row"
class="flex flex-col flex-wrap gap-x-5 gap-y-5 border-b border-gray-300 align-top last:border-0 md:flex-row"
>
<FeedbackSummaryBox
:feedback-count="feebackSummary.total_responses"

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

@ -1,29 +1,16 @@
<template>
<AccountMenuContent
:course-sessions="courseSessionsStore.allCurrentCourseSessions"
:selected-course-session="courseSessionsStore.currentCourseSession?.id"
:user="userStore"
@logout="logout"
@select-course-session="selectCourseSession"
@close="emit('close')"
/>
<AccountMenuContent :user="userStore" @logout="logout" @close="emit('close')" />
</template>
<script setup lang="ts">
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types";
const emit = defineEmits(["close"]);
const logout = () => {
userStore.handleLogout();
};
const selectCourseSession = (courseSession: CourseSession) => {
courseSessionsStore.switchCourseSessionById(courseSession.id);
};
const courseSessionsStore = useCourseSessionsStore();
const userStore = useUserStore();
</script>

View File

@ -1,32 +1,28 @@
<script setup lang="ts">
import CourseSessionsMenu from "@/components/header/CourseSessionsMenu.vue";
import { SETTINGS_ROUTE } from "@/router/names";
import type { User } from "@/stores/user";
import type { CourseSession } from "@/types";
import { useRouteLookups } from "@/utils/route";
import { computed } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
courseSessions: CourseSession[];
defineProps<{
user: User;
selectedCourseSession?: string;
}>();
const emit = defineEmits(["selectCourseSession", "logout", "close"]);
const emit = defineEmits(["logout", "close"]);
const router = useRouter();
const { inCourse } = useRouteLookups();
const showCourseSessionMenu = computed(() => inCourse() && props.courseSessions.length);
async function navigate(routeName: string) {
await router.push({ name: routeName });
emit("close");
}
const settingsRoute = {
name: SETTINGS_ROUTE,
};
</script>
<template>
<div class="text-black">
<div class="flex flex-col gap-4 text-black">
<div class="border-b py-4">
<div class="flex justify-start">
<div v-if="user.avatar_url">
@ -46,22 +42,19 @@ async function navigate(routeName: string) {
</div>
</div>
<div v-if="showCourseSessionMenu" class="border-b py-4">
<CourseSessionsMenu
:items="courseSessions"
:selected="selectedCourseSession"
@select="emit('selectCourseSession', $event)"
/>
</div>
<router-link class="flex items-center gap-2" :to="settingsRoute">
<it-icon-settings />
{{ $t("a.Einstellungen") }}
</router-link>
<button
type="button"
class="mt-6 flex items-center"
class="flex items-center gap-2"
data-cy="logout-button"
@click="emit('logout')"
>
<it-icon-logout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
<span>{{ $t("mainNavigation.logout") }}</span>
</button>
</div>
</template>

View File

@ -15,7 +15,7 @@ const { t } = useTranslation();
<nav class="bg-yellow-500">
<div class="mx-auto px-4 lg:px-8">
<div
class="relative flex h-16 w-full flex-col items-center justify-end space-x-8 lg:flex-row lg:items-stretch lg:justify-center"
class="relative flex h-auto min-h-16 w-full flex-col items-center justify-end space-x-8 lg:flex-row lg:items-stretch lg:justify-center"
>
<span class="flex items-center px-1 pt-1 font-bold text-black">
{{ t("a.Vorschau Teilnehmer") }} ({{ courseSession.title }})

View File

@ -10,11 +10,12 @@ import {
} from "@/utils/utils";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
import SelectedCourseSession from "./SelectedCourseSession.vue";
const { t } = useTranslation();
const courseSessionsStore = useCourseSessionsStore();
const { inCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
const { isInCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
useRouteLookups();
const {
hasCompetenceNaviMenu,
@ -30,13 +31,18 @@ const mentorTabTitle = computed(() =>
);
</script>
<template>
<div v-if="courseSessionsStore.currentCourseSession" class="hidden space-x-8 lg:flex">
<div
v-if="courseSessionsStore.currentCourseSession"
class="flex space-x-8 px-2 lg:px-10"
>
<SelectedCourseSession />
<router-link
v-if="hasCockpitMenu"
data-cy="navigation-cockpit-link"
:to="getCockpitUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item"
:class="{ 'nav-item--active': inCockpit() }"
class="nav-item-no-mobile"
:class="{ 'nav-item--active': isInCockpit }"
>
{{ t("cockpit.title") }}
</router-link>
@ -46,7 +52,7 @@ const mentorTabTitle = computed(() =>
data-cy="navigation-preview-link"
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
target="_blank"
class="nav-item"
class="nav-item-no-mobile"
>
<div class="flex items-center">
<span>{{ t("a.Vorschau Teilnehmer") }}</span>
@ -57,7 +63,7 @@ const mentorTabTitle = computed(() =>
v-if="hasLearningPathMenu"
data-cy="navigation-learning-path-link"
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item"
class="nav-item-no-mobile"
:class="{ 'nav-item--active': inLearningPath() }"
>
{{ t("general.learningPath") }}
@ -67,7 +73,7 @@ const mentorTabTitle = computed(() =>
v-if="hasCompetenceNaviMenu"
data-cy="navigation-competence-profile-link"
:to="getCompetenceNaviUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item"
class="nav-item-no-mobile"
:class="{ 'nav-item--active': inCompetenceProfile() }"
>
{{ t("competences.title") }}
@ -77,7 +83,7 @@ const mentorTabTitle = computed(() =>
v-if="hasLearningMentor"
data-cy="navigation-learning-mentor-link"
:to="getLearningMentorUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item"
class="nav-item-no-mobile"
:class="{ 'nav-item--active': inLearningMentor() }"
>
{{ t(mentorTabTitle) }}

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useCourseSessionsStore } from "@/stores/courseSessions";
import CoursePreviewBar from "./CoursePreviewBar.vue";
import MainNavigationBar from "./MainNavigationBar.vue";
const courseSessionsStore = useCourseSessionsStore();

View File

@ -1,12 +1,24 @@
<script setup lang="ts">
import { useRouteLookups } from "@/utils/route";
import { useTranslation } from "i18next-vue";
const { isInCourse } = useRouteLookups();
const { t } = useTranslation();
</script>
<template>
<div class="hidden flex-shrink-0 items-center lg:flex">
<div class="flex items-center">
<div class="flex flex-shrink-0 items-center">
<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 class="fill-current text-slate-500" />
<span class="hidden text-slate-500 lg:inline">
{{ t("a.Dashboard") }}
</span>
</router-link>
</div>
</template>
<template v-else>
<router-link to="/" class="flex">
<it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" />
</router-link>
@ -15,6 +27,6 @@ const { t } = useTranslation();
{{ t("general.title") }}
</div>
</router-link>
</div>
</template>
</div>
</template>

View File

@ -15,12 +15,7 @@ log.debug("MainNavigationBar created");
const courseSessionsStore = useCourseSessionsStore();
const { inMediaLibrary, inAppointments } = useRouteLookups();
const { hasMediaLibraryMenu, hasAppointmentsMenu, hasSessionTitle } =
useNavigationAttributes();
const selectedCourseSessionTitle = computed(() => {
return courseSessionsStore.currentCourseSession?.title;
});
const { hasMediaLibraryMenu, hasAppointmentsMenu } = useNavigationAttributes();
const appointmentsUrl = computed(() => {
const currentCourseSession = courseSessionsStore.currentCourseSession;
@ -40,13 +35,12 @@ onMounted(() => {
<nav class="bg-blue-900 text-white">
<div class="mx-auto px-4 lg:px-8">
<div class="relative flex h-16 justify-between">
<MobileMenuButton />
<div class="flex flex-1 items-stretch justify-start">
<HomeNavigation />
<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="
@ -62,7 +56,7 @@ onMounted(() => {
v-if="hasAppointmentsMenu"
:to="appointmentsUrl"
data-cy="all-duedates-link"
class="nav-item"
class="nav-item-no-mobile"
:class="{ 'nav-item--active': inAppointments() }"
>
<it-icon-calendar-light class="h-8 w-8" />
@ -71,22 +65,12 @@ onMounted(() => {
<!-- Notification Bell & Menu -->
<NotificationButton />
<div
v-if="hasSessionTitle"
class="nav-item hidden items-center lg:inline-flex"
>
<div class="" data-cy="current-course-session-title">
{{ selectedCourseSessionTitle }}
</div>
</div>
<div class="nav-item">
<ProfileMenuButton />
</div>
</div>
<MobileMenuButton />
</div>
</div>
</nav>
</template>
<style lang="postcss"></style>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useVVByLink } from "@/composables";
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";
@ -49,13 +50,20 @@ const mentorTabTitle = computed(() =>
? "a.Praxisbildner"
: "a.Lernbegleitung"
);
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
@ -66,62 +74,80 @@ const mentorTabTitle = computed(() =>
</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="text-sm text-gray-900">{{ courseSession.course.title }}</h4>
<ul class="mt-6">
<li v-if="hasCockpitMenu" class="mb-6">
<button
<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="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-cockpit-link"
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
:to="getCockpitUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
{{ $t("cockpit.title") }}
</button>
</router-link>
</li>
<li v-if="hasPreviewMenu" class="mb-6">
<button
<li v-if="hasPreviewMenu">
<router-link
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-preview-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
:to="getLearningPathUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
{{ $t("a.Vorschau Teilnehmer") }}
</button>
</router-link>
</li>
<li v-if="hasLearningPathMenu" class="mb-6">
<button
<li v-if="hasLearningPathMenu">
<router-link
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"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
:to="getLearningPathUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
{{ $t("general.learningPath") }}
</button>
</router-link>
</li>
<li v-if="hasCompetenceNaviMenu" class="mb-6">
<button
<li v-if="hasCompetenceNaviMenu">
<router-link
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"
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
:to="getCompetenceNaviUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
{{ $t("competences.title") }}
</button>
</router-link>
</li>
<li v-if="hasLearningMentor" class="mb-6">
<button
<li v-if="hasLearningMentor">
<router-link
class="block w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-mentor-link"
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))"
:to="getLearningMentorUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
{{ $t(mentorTabTitle) }}
</button>
</router-link>
</li>
<li v-if="hasMediaLibraryMenu" class="mb-6">
<button
data-cy="medialibrary-link"
@click="clickLink(getMediaCenterUrl(courseSession.course.slug))"
>
{{ $t("a.Mediathek") }}
</button>
</li>
<li
v-if="isVVLearningMentor(courseSessionsStore.currentCourseSession)"
class="mb-6"
@ -135,22 +161,57 @@ const mentorTabTitle = computed(() =>
</li>
</ul>
</div>
<div class="mt-6 border-b">
<ul>
<li class="mb-6">
<button data-cy="dashboard-link" @click="clickLink('/')">myVBV</button>
<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"
active-class="bg-gray-200 text-blue-900 font-bold"
:to="getMediaCenterUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
<it-icon-media-library />
{{ $t("a.Mediathek") }}
</router-link>
</li>
<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"
active-class="bg-gray-200 text-blue-900 font-bold"
:to="'/'"
@click="$emit('closemodal')"
>
<!-- todo: correct route -->
<it-icon-calendar-light />
{{ $t("a.Termine") }}
</router-link>
</li>
</ul>
</div>
<button
v-if="user?.loggedIn"
type="button"
class="mt-6 flex items-center"
@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

@ -45,7 +45,7 @@ const showMenu = ref(false);
@logout="userStore.handleLogout()"
/>
</Teleport>
<div class="absolute inset-y-0 left-0 flex items-center lg:hidden">
<div class="inset-y-0 flex items-center lg:hidden">
<!-- Mobile menu button -->
<div data-cy="navigation-mobile-menu-button" class="flex" @click="showMenu = true">
<button

View File

@ -29,7 +29,11 @@ function popoverClick(event: Event) {
<AccountMenu @close="showMenu = false" />
</ItFullScreenModal>
</Teleport>
<div v-if="userStore.loggedIn" class="flex items-center" data-cy="header-profile">
<div
v-if="userStore.loggedIn"
class="hidden items-center lg:flex"
data-cy="header-profile"
>
<Popover class="relative">
<PopoverButton @click="popoverClick($event)">
<div v-if="userStore.avatar_url">

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession } from "@/types";
import { useNavigationAttributes } from "@/utils/navigation";
import { useRouteLookups } from "@/utils/route";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { computed } from "vue";
import CourseSessionsMenu from "./CourseSessionsMenu.vue";
const { isInCourse } = useRouteLookups();
const courseSessionsStore = useCourseSessionsStore();
const { hasSessionTitle } = useNavigationAttributes();
const selectedCourseSessionTitle = computed(() => {
return courseSessionsStore.currentCourseSession?.title;
});
const selectedCourseSession = computed(() => {
return courseSessionsStore.currentCourseSession;
});
const selectCourseSession = (courseSession: CourseSession) => {
courseSessionsStore.switchCourseSessionById(courseSession.id);
};
const courseSessions = computed(() => {
return courseSessionsStore.allCourseSessions;
});
const showCourseSessionMenu = computed(
() => isInCourse.value && courseSessions.value.length
);
</script>
<template>
<div
v-if="hasSessionTitle"
class="nav-item-base inline-flex items-center lg:inline-flex"
>
<Popover v-if="showCourseSessionMenu" class="relative">
<PopoverButton
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">
<div
class="flex flex-col rounded-lg bg-white p-4 shadow-lg ring-1 ring-black/5"
>
<h3 class="fond-bold mb-2 text-base text-black">Durchführung</h3>
<CourseSessionsMenu
:items="courseSessions"
:selected="selectedCourseSession?.id"
@select="selectCourseSession"
/>
</div>
</PopoverPanel>
</Popover>
<div v-else data-cy="current-course-session-title" class="text-bold">
{{ selectedCourseSessionTitle }}
</div>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterLink } from "vue-router";
// https://router.vuejs.org/guide/advanced/extending-router-link
import { isExternalLink as isExternalLinkFn } from "@/utils/navigation";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
// @ts-expect-error the example above mentions needing @ts-ignore
...RouterLink.props,
});
const isExternalLink = computed(() => {
return isExternalLinkFn(props.to);
});
</script>
<template>
<a
v-if="isExternalLink"
class="flex items-center gap-2"
v-bind="$attrs"
:href="to"
target="_blank"
>
<slot />
<it-icon-external-link class="h-6 w-6" />
</a>
<!-- make `:to` explicit -->
<router-link
v-else
v-slot="{ isActive, href, navigate }"
v-bind="$props"
:to="$props.to"
custom
>
<a
v-bind="$attrs"
:class="isActive ? activeClass : ''"
:href="href"
@click="navigate"
>
<slot />
</a>
</router-link>
</template>

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import SubNavItem from "@/components/header/SubNavItem.vue";
import { isExternalLink } from "@/utils/navigation";
import { Listbox, ListboxOption, ListboxOptions } from "@headlessui/vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
export interface EntryRoute {
name: string;
}
export type EntryOrExternalRoute = EntryRoute | string;
export interface SubNavEntry {
id: number;
name: string;
route: EntryOrExternalRoute;
dataCy?: string;
}
export interface Props {
items: SubNavEntry[];
}
const props = defineProps<Props>();
const isCurrentRoute = (route: { name: string } | string) => {
return typeof route !== "string" && route?.name === router.currentRoute.value.name;
};
const currentRouteName = computed(() => {
return props.items.find((item) => isCurrentRoute(item.route))?.name || "";
});
const open = ref<boolean>(false);
const currentRoute = ref(props.items.find((item) => isCurrentRoute(item.route)));
const selectRoute = (current: SubNavEntry) => {
// we use this to mimic VueRouter's active flag
open.value = false;
currentRoute.value = current;
};
const internalLinks = computed(() => {
return props.items.filter((i) => !isExternalLink(i.route));
});
const externalLinks = computed(() => {
return props.items.filter((i) => isExternalLink(i.route));
});
</script>
<template>
<nav class="border-b bg-white px-4 lg:px-8">
<Listbox as="div" :model-value="currentRoute" by="id">
<div class="relative w-full py-2 lg:hidden">
<button
class="relative flex w-full cursor-default flex-row items-center border bg-white py-3 pl-5 pr-10 text-left"
@click="open = !open"
>
{{ currentRouteName }}
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
</span>
</button>
<ListboxOptions
v-if="open"
class="absolute top-14 z-50 flex w-full cursor-default flex-col rounded-xl border-0 bg-white text-left shadow-lg"
static
>
<ListboxOption
v-for="item in items"
:key="item.id"
v-slot="{ selected }"
:value="item"
class="relative w-full border-b py-3 pl-10 pr-10 last:border-b-0"
>
<SubNavItem
:to="item.route"
class="flex items-center gap-2"
@click="selectRoute(item)"
>
<it-icon-check
v-if="selected"
class="absolute left-2 top-1/2 w-8 -translate-y-1/2"
/>
{{ item.name }}
</SubNavItem>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
<div class="center hidden items-end justify-between lg:flex">
<ul class="flex flex-row gap-10">
<li
v-for="item in internalLinks"
:key="item.id"
class="border-t-2 border-t-transparent"
:class="{ 'border-b-2 border-b-blue-900': isCurrentRoute(item.route) }"
>
<SubNavItem :data-cy="item.dataCy" :to="item.route" class="block py-3">
{{ item.name }}
</SubNavItem>
</li>
</ul>
<ul class="flex flex-row gap-10">
<li
v-for="item in externalLinks"
:key="item.id"
class="border-b-2 border-t-2 border-b-transparent border-t-transparent"
>
<SubNavItem :data-cy="item.dataCy" :to="item.route" class="block py-3">
{{ item.name }}
</SubNavItem>
</li>
</ul>
</div>
</nav>
</template>

View File

@ -26,14 +26,18 @@ const hasPendingTasks = computed(() => props.pendingTasks > 0);
<div
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="flex flex-grow flex-row items-center justify-start">
<div
class="flex flex-grow flex-row flex-wrap items-center justify-start space-y-4 sm:space-y-0"
>
<div class="w-80">
<div class="font-bold">{{ taskTitle }}</div>
<div class="text-small text-gray-900">
{{ $t("a.Circle") }} «{{ circleTitle }}»
</div>
</div>
<div class="flex flex-grow flex-row items-center justify-start space-x-2 pl-20">
<div
class="flex flex-grow flex-row items-center justify-start space-x-2 sm:pl-20"
>
<template v-if="hasPendingTasks">
<div
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-sm font-bold"

View File

@ -37,6 +37,7 @@ const circles = computed(() => {
(c) => props.showCircleSlugs?.includes(c.slug) ?? true
);
}
return lpQueryResult.circles.value ?? [];
});
@ -45,7 +46,10 @@ const wrapperClasses = computed(() => {
if (props.diagramType === "horizontal") {
classes += " flex-row h-8 space-x-2";
} else if (props.diagramType === "horizontalSmall") {
classes += " flex-row h-5 space-x-1";
classes +=
filteredCircles.value.length > 15
? " flex-row h-3 space-x-[1.1px]"
: " flex-row h-4 space-x-[1.8px]";
} else if (props.diagramType === "singleSmall") {
classes += " h-8";
}

View File

@ -10,7 +10,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
<PopoverPanel>
<div
class="absolute right-0 z-10 mt-2 bg-white px-4 py-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none lg:right-2"
class="absolute right-0 z-30 mt-2 bg-white px-4 py-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none lg:right-2"
>
<!-- To close the popover withing your content, use the 'PopoverButton'
https://headlessui.com/vue/popover#closing-popovers-manually

View File

@ -32,5 +32,23 @@ const paymentMethods = [
<p class="mt-4">
{{ $t("shop.paymentCembraByjunoMessage") }}
</p>
<p class="mt-4">
<i18next
:translation="
$t('a.Es gelten die {cembraTos} und die {cembraPrivacy} der CembraPay AG.')
"
>
<template #cembraTos>
<a :href="t('cembraTosLink')" target="_blank" class="underline">
{{ $t("a.AGB") }}
</a>
</template>
<template #cembraPrivacy>
<a :href="t('cembraPrivacyLink')" target="_blank" class="underline">
{{ $t("a.Datenschutzerklärung") }}
</a>
</template>
</i18next>
</p>
</div>
</template>

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

@ -39,7 +39,7 @@ const removeNoScroll = () => {
<div
v-if="show"
data-cy="full-screen-modal"
class="fixed top-0 h-full w-full overflow-y-scroll bg-white px-4 py-16 lg:px-16 lg:py-24"
class="fixed top-0 z-20 h-full w-full overflow-y-scroll bg-white px-4 py-16 lg:px-16 lg:py-24"
>
<button
type="button"

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="flex flex-col sm:flex-row">
<div class="flex-none border-r bg-white p-4 lg:p-8">
<slot name="side"></slot>
</div>

View File

@ -722,13 +722,25 @@ export function useVVByLink() {
return { href };
}
export function useAllCompetenceCertificates(userId: string, courseSlug: string) {
export function useAllCompetenceCertificates(
userId: string,
courseSlug: string,
currentCourseSessionOnly: boolean = false
) {
const courseSessionsStore = useCourseSessionsStore();
const certificateQueries = courseSessionsStore.allCourseSessions.map(
(courseSession) => {
let certificateQueries;
if (currentCourseSessionOnly) {
const courseSession = useCurrentCourseSession();
certificateQueries = [
useCertificateQuery([userId], courseSlug, courseSession.value).certificatesQuery,
];
} else {
// wtf
certificateQueries = courseSessionsStore.allCourseSessions.map((courseSession) => {
// todo: use a single query, instead of one for every courseSession
return useCertificateQuery([userId], courseSlug, courseSession).certificatesQuery;
}
);
});
}
const competenceCertificatesPerCs = computed(() =>
certificateQueries.map((query) => {

View File

@ -662,6 +662,23 @@ export type LearningContentEdoniqTestObjectType = CoursePageInterface & Learning
translation_key: Scalars['String']['output'];
};
export type LearningContentFeedbackAutomobilGewerbeObjectType = CoursePageInterface & LearningContentInterface & {
__typename?: 'LearningContentFeedbackAutomobilGewerbeObjectType';
can_user_self_toggle_course_completion: Scalars['Boolean']['output'];
circle?: Maybe<CircleLightObjectType>;
content_type: Scalars['String']['output'];
content_url: Scalars['String']['output'];
course?: Maybe<CourseObjectType>;
description: Scalars['String']['output'];
frontend_url: Scalars['String']['output'];
id: Scalars['ID']['output'];
live: Scalars['Boolean']['output'];
minutes?: Maybe<Scalars['Int']['output']>;
slug: Scalars['String']['output'];
title: Scalars['String']['output'];
translation_key: Scalars['String']['output'];
};
export type LearningContentFeedbackUkObjectType = CoursePageInterface & LearningContentInterface & {
__typename?: 'LearningContentFeedbackUKObjectType';
can_user_self_toggle_course_completion: Scalars['Boolean']['output'];
@ -973,6 +990,7 @@ export type Query = {
learning_content_assignment?: Maybe<LearningContentAssignmentObjectType>;
learning_content_attendance_course?: Maybe<LearningContentAttendanceCourseObjectType>;
learning_content_document_list?: Maybe<LearningContentDocumentListObjectType>;
learning_content_feedback_automobil_gewerbe?: Maybe<LearningContentFeedbackAutomobilGewerbeObjectType>;
learning_content_feedback_uk?: Maybe<LearningContentFeedbackUkObjectType>;
learning_content_feedback_vv?: Maybe<LearningContentFeedbackVvObjectType>;
learning_content_knowledge_assessment?: Maybe<LearningContentKnowledgeAssessmentObjectType>;
@ -1206,6 +1224,8 @@ type CoursePageFieldsLearningContentDocumentListObjectTypeFragment = { __typenam
type CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment = { __typename?: 'LearningContentEdoniqTestObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment' };
type CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment = { __typename?: 'LearningContentFeedbackAutomobilGewerbeObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment' };
type CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment = { __typename?: 'LearningContentFeedbackUKObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment' };
type CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment = { __typename?: 'LearningContentFeedbackVVObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment' };
@ -1232,7 +1252,7 @@ type CoursePageFieldsPerformanceCriteriaObjectTypeFragment = { __typename?: 'Per
type CoursePageFieldsTopicObjectTypeFragment = { __typename?: 'TopicObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsTopicObjectTypeFragment' };
export type CoursePageFieldsFragment = CoursePageFieldsActionCompetenceObjectTypeFragment | CoursePageFieldsAssignmentObjectTypeFragment | CoursePageFieldsCircleObjectTypeFragment | CoursePageFieldsCompetenceCertificateListObjectTypeFragment | CoursePageFieldsCompetenceCertificateObjectTypeFragment | CoursePageFieldsLearningContentAssignmentObjectTypeFragment | CoursePageFieldsLearningContentAttendanceCourseObjectTypeFragment | CoursePageFieldsLearningContentDocumentListObjectTypeFragment | CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment | CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment | CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment | CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment | CoursePageFieldsLearningContentLearningModuleObjectTypeFragment | CoursePageFieldsLearningContentMediaLibraryObjectTypeFragment | CoursePageFieldsLearningContentPlaceholderObjectTypeFragment | CoursePageFieldsLearningContentRichTextObjectTypeFragment | CoursePageFieldsLearningContentVideoObjectTypeFragment | CoursePageFieldsLearningPathObjectTypeFragment | CoursePageFieldsLearningSequenceObjectTypeFragment | CoursePageFieldsLearningUnitObjectTypeFragment | CoursePageFieldsPerformanceCriteriaObjectTypeFragment | CoursePageFieldsTopicObjectTypeFragment;
export type CoursePageFieldsFragment = CoursePageFieldsActionCompetenceObjectTypeFragment | CoursePageFieldsAssignmentObjectTypeFragment | CoursePageFieldsCircleObjectTypeFragment | CoursePageFieldsCompetenceCertificateListObjectTypeFragment | CoursePageFieldsCompetenceCertificateObjectTypeFragment | CoursePageFieldsLearningContentAssignmentObjectTypeFragment | CoursePageFieldsLearningContentAttendanceCourseObjectTypeFragment | CoursePageFieldsLearningContentDocumentListObjectTypeFragment | CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment | CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment | CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment | CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment | CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment | CoursePageFieldsLearningContentLearningModuleObjectTypeFragment | CoursePageFieldsLearningContentMediaLibraryObjectTypeFragment | CoursePageFieldsLearningContentPlaceholderObjectTypeFragment | CoursePageFieldsLearningContentRichTextObjectTypeFragment | CoursePageFieldsLearningContentVideoObjectTypeFragment | CoursePageFieldsLearningPathObjectTypeFragment | CoursePageFieldsLearningSequenceObjectTypeFragment | CoursePageFieldsLearningUnitObjectTypeFragment | CoursePageFieldsPerformanceCriteriaObjectTypeFragment | CoursePageFieldsTopicObjectTypeFragment;
export type AttendanceCheckQueryQueryVariables = Exact<{
courseSessionId: Scalars['ID']['input'];
@ -1276,6 +1296,9 @@ export type CompetenceCertificateQueryQuery = { __typename?: 'Query', competence
) | (
{ __typename?: 'LearningContentEdoniqTestObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment': CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackAutomobilGewerbeObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment': CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackUKObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment': CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment } }
@ -1330,6 +1353,9 @@ export type CompetenceCertificateForUserQueryQuery = { __typename?: 'Query', com
) | (
{ __typename?: 'LearningContentEdoniqTestObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment': CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackAutomobilGewerbeObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment': CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackUKObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment': CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment } }
@ -1407,6 +1433,9 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: '
& { ' $fragmentRefs'?: { 'CoursePageFieldsCompetenceCertificateObjectTypeFragment': CoursePageFieldsCompetenceCertificateObjectTypeFragment } }
) | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment': CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackAutomobilGewerbeObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment': CoursePageFieldsLearningContentFeedbackAutomobilGewerbeObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentFeedbackUKObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment': CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment } }

View File

@ -13,6 +13,7 @@ type Query {
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
learning_content_feedback_uk: LearningContentFeedbackUKObjectType
learning_content_feedback_vv: LearningContentFeedbackVVObjectType
learning_content_feedback_automobil_gewerbe: LearningContentFeedbackAutomobilGewerbeObjectType
learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType
@ -819,6 +820,22 @@ type LearningContentFeedbackVVObjectType implements CoursePageInterface & Learni
circle: CircleLightObjectType
}
type LearningContentFeedbackAutomobilGewerbeObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
minutes: Int
description: String!
content_url: String!
can_user_self_toggle_course_completion: Boolean!
circle: CircleLightObjectType
}
type LearningContentLearningModuleObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!

View File

@ -59,6 +59,7 @@ export const LearningContentAssignmentObjectType = "LearningContentAssignmentObj
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
export const LearningContentFeedbackAutomobilGewerbeObjectType = "LearningContentFeedbackAutomobilGewerbeObjectType";
export const LearningContentFeedbackUKObjectType = "LearningContentFeedbackUKObjectType";
export const LearningContentFeedbackVVObjectType = "LearningContentFeedbackVVObjectType";
export const LearningContentInterface = "LearningContentInterface";

View File

@ -8,6 +8,7 @@
"a.Abgabetermin": "Abgabetermin",
"a.Abgezogene Punkte": "Abgezogene Punkte",
"a.Adresse": "Adresse",
"a.AGB": "AGB",
"a.Aktuell begleitest du niemanden als Lernbegleitung.": "Aktuell begleitest du niemanden als Lernbegleitung.",
"a.Aktuell begleitest du niemanden als Praxisbildner.": "Aktuell begleitest du niemanden als Praxisbildner.",
"a.Aktuell bist du leider keiner Durchführung zugewiesen.": "Aktuell bist du leider keiner Durchführung zugewiesen.",
@ -33,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",
@ -69,9 +71,11 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Damit du myVBV nutzen kannst, brauchst du ein Konto.",
"a.Das muss ich nochmals anschauen": "Das muss ich nochmals anschauen",
"a.Das wurde mit dir geteilt": "Das wurde mit dir geteilt",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Datei auswählen",
"a.Datei hochladen": "Datei hochladen",
"a.Datei kann nicht gespeichert werden.": "Datei kann nicht gespeichert werden.",
"a.Datenschutzerklärung": "Datenschutzerklärung",
"a.Datum": "Datum",
"a.Debit-/Kreditkarte/Twint": "Debit-/Kreditkarte/Twint",
"a.Dein Feedback für x y wurde freigegeben.": "Dein Feedback für {{x}} {{y}} wurde freigegeben.",
@ -81,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",
@ -92,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.",
@ -102,6 +109,7 @@
"a.E-Mail Adresse": "E-Mail Adresse",
"a.Einladung": "Einladung",
"a.Einladung abschicken": "Einladung abschicken",
"a.Einstellungen": "Einstellungen",
"a.Elemente zu erledigen": "Elemente zu erledigen",
"a.Email": "Email",
"a.Entfernen": "Entfernen",
@ -113,6 +121,7 @@
"a.Ergebnisse bewerten": "Ergebnisse bewerten",
"a.Ergebnisse teilen": "Ergebnisse teilen",
"a.Erneut bearbeiten": "Erneut bearbeiten",
"a.Es gelten die {cembraTos} und die {cembraPrivacy} der CembraPay AG.": "Es gelten die {cembraTos} und die {cembraPrivacy} der CembraPay AG.",
"a.Experte": "Experte",
"a.Feedback abschliessen": "Feedback abschliessen",
"a.Feedback ansehen": "Feedback ansehen",
@ -208,6 +217,7 @@
"a.Nicht bestanden": "Nicht bestanden",
"a.Nicht bewertet": "Nicht bewertet",
"a.Nichtleben": "Nichtleben",
"a.Noch nicht bestätigt": "Noch nicht bestätigt",
"a.Note": "Note",
"a.NUMBER Elemente abgeschlossen": "{NUMBER} Elemente abgeschlossen",
"a.NUMBER Präsenztage abgeschlossen": "{NUMBER} Präsenztage abgeschlossen",
@ -219,6 +229,7 @@
"a.Personen, die du begleitest": "Personen, die du begleitest ",
"a.Persönliche Informationen": "Persönliche Informationen",
"a.PLZ": "PLZ",
"a.Postleizahl hat das falsche Format": "Postleizahl hat das falsche Format",
"a.Praxisauftrag": "Praxisauftrag",
"a.Praxisaufträge anschauen": "Praxisaufträge anschauen",
"a.Praxisbildner": "Praxisbildner",
@ -263,6 +274,7 @@
"a.Teilnehmer": "Teilnehmer",
"a.Teilnehmer im": "Teilnehmer im",
"a.Teilnehmer nach Zulassungsprofilen im": "Teilnehmer nach Zulassungsprofilen im",
"a.Teilnehmer Vorschau": "Teilnehmer Vorschau",
"a.Telefonnummer": "Telefonnummer",
"a.Telefonnummer hat das falsche Format": "Telefonnummer hat das falsche Format",
"a.Termin": "Termin",
@ -302,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",
@ -349,6 +362,8 @@
"Berufsbildner": "Berufsbildner",
"Bestanden": "Bestanden",
"Bewertung von x y": "Bewertung von {{x}} {{y}}",
"cembraPrivacyLink": "https://cembrapay.ch/de/privacy",
"cembraTosLink": "https://cembrapay.ch/de/terms/CP",
"Circle": "Circle",
"circlePage.circleContentBoxTitle": "Das lernst du in diesem Circle",
"circlePage.contactExpertButton": "Trainer/-in kontaktieren",

View File

@ -8,6 +8,7 @@
"a.Abgabetermin": "Date de remise",
"a.Abgezogene Punkte": "Points déduits",
"a.Adresse": "Adresse",
"a.AGB": "CGV",
"a.Aktuell begleitest du niemanden als Lernbegleitung.": "Actuellement, vous n'accompagnez personne en tant que mentor d'apprentissage.",
"a.Aktuell begleitest du niemanden als Praxisbildner.": "Actuellement, vous n'accompagnez personne en tant que formateur/-trice pratique.",
"a.Aktuell bist du leider keiner Durchführung zugewiesen.": "Actuellement, vous n'êtes malheureusement affecté à aucune session.",
@ -33,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",
@ -69,9 +71,11 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Pour utiliser myVBV, vous devez créer un compte.",
"a.Das muss ich nochmals anschauen": "Il faut que je regarde cela encore une fois de plus près",
"a.Das wurde mit dir geteilt": "Cela a été partagé avec toi",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Sélectionner le fichier",
"a.Datei hochladen": "Télécharger le fichier",
"a.Datei kann nicht gespeichert werden.": "Impossible d'enregistrer le fichier.",
"a.Datenschutzerklärung": "protection des données",
"a.Datum": "Date",
"a.Debit-/Kreditkarte/Twint": "Carte de débit/crédit / Twint",
"a.Dein Feedback für x y wurde freigegeben.": "Ton feedback pour {{x}} {{y}} a été validé.",
@ -81,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",
@ -92,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.",
@ -102,6 +109,7 @@
"a.E-Mail Adresse": "Adresse e-mail",
"a.Einladung": "Invitation",
"a.Einladung abschicken": "Envoyer l'invitation",
"a.Einstellungen": "Paramètres",
"a.Elemente zu erledigen": "Eléments à faire",
"a.Email": "Email",
"a.Entfernen": "Supprimer",
@ -113,6 +121,7 @@
"a.Ergebnisse bewerten": "Évaluer les résultats",
"a.Ergebnisse teilen": "Partager les résultats",
"a.Erneut bearbeiten": "Modifier à nouveau",
"a.Es gelten die {cembraTos} und die {cembraPrivacy} der CembraPay AG.": "Les {cembraTos} et la déclaration de {cembraPrivacy} de CembraPay AG s'appliquent.",
"a.Experte": "Expert",
"a.Feedback abschliessen": "Terminer le feedback",
"a.Feedback ansehen": "Voir le feedback",
@ -208,6 +217,7 @@
"a.Nicht bestanden": "Échoué",
"a.Nicht bewertet": "Non évalué",
"a.Nichtleben": "Non-vie",
"a.Noch nicht bestätigt": "Pas encore confirmé",
"a.Note": "Note",
"a.NUMBER Elemente abgeschlossen": "{NUMBER} éléments terminés",
"a.NUMBER Präsenztage abgeschlossen": "{NUMBER} jours de présence complétés",
@ -219,6 +229,7 @@
"a.Personen, die du begleitest": "Personnes que tu accompagnes",
"a.Persönliche Informationen": "Informations personnelles",
"a.PLZ": "Code postal",
"a.Postleizahl hat das falsche Format": "Le code postal n'a pas le bon format",
"a.Praxisauftrag": "Exercice pratique",
"a.Praxisaufträge anschauen": "Voir les missions pratiques",
"a.Praxisbildner": "Formateur pratique",
@ -263,6 +274,7 @@
"a.Teilnehmer": "Participants",
"a.Teilnehmer im": "Participants en",
"a.Teilnehmer nach Zulassungsprofilen im": "Participants par profil d'admission en",
"a.Teilnehmer Vorschau": "Aperçu des participants",
"a.Telefonnummer": "Numéro de téléphone",
"a.Telefonnummer hat das falsche Format": "Le numéro de téléphone n'est pas au bon format",
"a.Termin": "Date",
@ -302,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",
@ -349,6 +362,8 @@
"Berufsbildner": "Formateur professionnel",
"Bestanden": "Réussi",
"Bewertung von x y": "Évaluation de {{x}} {{y}}",
"cembraPrivacyLink": "https://cembrapay.ch/fr/privacy",
"cembraTosLink": "https://cembrapay.ch/fr/terms/CP",
"Circle": "Cercle",
"circlePage.circleContentBoxTitle": "Ce que tu vas apprendre dans ce Circle",
"circlePage.contactExpertButton": "Contacter le formateur / la formatrice",

View File

@ -33,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",
@ -69,6 +70,7 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Per utilizzare myVBV, hai bisogno di un account.",
"a.Das muss ich nochmals anschauen": "Devo riguardarlo ancora una volta",
"a.Das wurde mit dir geteilt": "Questo è stato condiviso con te",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Selezionare il file",
"a.Datei hochladen": "Carica il file",
"a.Datei kann nicht gespeichert werden.": "Impossibile salvare il file.",
@ -81,6 +83,8 @@
"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 {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",
@ -92,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.",
@ -102,6 +107,7 @@
"a.E-Mail Adresse": "Indirizzo e-mail",
"a.Einladung": "Invito",
"a.Einladung abschicken": "Inviare l'invito",
"a.Einstellungen": "Impostazioni",
"a.Elemente zu erledigen": "Elementi da completare",
"a.Email": "E-mail",
"a.Entfernen": "Rimuovere",
@ -302,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",

View File

@ -34,6 +34,10 @@
v-else-if="feedbackType === 'uk'"
:feedback-data="feedbackData"
/>
<FeedbackPageAutomobilgewerbe
v-else-if="feedbackType === 'automobilgewerbe'"
:feedback-data="feedbackData"
/>
</main>
</div>
</div>
@ -42,6 +46,7 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import { itGet } from "@/fetchHelpers";
import FeedbackPageAutomobilgewerbe from "@/pages/cockpit/FeedbackPageAutomobilgewerbe.vue";
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
@ -84,7 +89,7 @@ onMounted(async () => {
log.debug("FeedbackPage feedbackData", feedbackData.value);
if (
feedbackData.value &&
["uk", "vv"].includes(feedbackData.value?.feedbackType ?? "")
["uk", "vv", "automobilgewerbe"].includes(feedbackData.value?.feedbackType ?? "")
) {
feedbackType.value = feedbackData.value.feedbackType;
}

View File

@ -0,0 +1,59 @@
<template>
<FeedbackResults
:ordered-questions="orderedQuestions"
:feedback-data="feedbackData"
:rating-keys="ratingKeys"
:vertical-chart-keys="verticalChartKeys"
:horizontal-chart-keys="horizontalChartKeys"
:open-keys="openKeys"
/>
</template>
<script setup lang="ts">
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
import type { FeedbackData } from "@/types";
import { useTranslation } from "i18next-vue";
import * as log from "loglevel";
defineProps<{
feedbackData: FeedbackData;
}>();
log.debug("FeedbackPageVV created");
const { t } = useTranslation();
const orderedQuestions = [
{
key: "satisfaction",
question: t("feedback.satisfactionLabel"),
},
{
key: "goal_attainment",
question: t("feedback.goalAttainmentLabel"),
},
{
key: "proficiency",
question: t("feedback.proficiencyLabelVV"),
},
{
key: "would_recommend",
question: t("feedback.recommendLabelVV"),
},
{
key: "course_negative_feedback",
question: t("feedback.courseNegativeFeedbackLabel"),
},
{
key: "course_positive_feedback",
question: t("feedback.coursePositiveFeedbackLabel"),
},
];
const ratingKeys = ["satisfaction", "goal_attainment"];
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
const horizontalChartKeys = ["proficiency"];
const openKeys = ["course_negative_feedback", "course_positive_feedback"];
</script>
<style scoped></style>

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,63 +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 justify-between bg-white p-6">
<ItDropdownSelect
v-model="state.attendanceCourseSelected"
:items="presenceCoursesDropdownOptions ?? []"
></ItDropdownSelect>
<div v-if="!state.attendanceSaved" class="flex flex-row items-center">
<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 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>
<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>
<div class="mt-4 flex flex-col bg-white p-6">
<div
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
<button
v-if="attendanceSaved"
class="col-span-1 mr-4 hidden justify-self-end lg:flex"
data-cy="export-button"
@click="exportData"
>
<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"
/>
<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' : ''"
@ -230,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

@ -8,8 +8,9 @@ import {
competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils";
import type { CompetenceCertificate } from "@/types";
import * as log from "loglevel";
import log from "loglevel";
import { computed } from "vue";
import CompetenceCertificateGrade from "./CompetenceCertificateGrade.vue";
log.debug("CompetenceCertificateComponent setup");
@ -85,34 +86,10 @@ const showCourseSession = computed(() => {
</h3>
</div>
<section v-if="userPointsEvaluatedAssignments > 0">
<div class="flex items-center">
<div
class="py-4"
:class="{ 'heading-1': props.detailView, 'heading-2': !props.detailView }"
:data-cy="`certificate-${competenceCertificate.slug}-grade`"
>
{{ $t("a.Note") }}: {{ userGrade }}
</div>
</div>
<div
class="text-gray-900"
:data-cy="`certificate-${competenceCertificate.slug}-grade-percent`"
>
{{ $t("a.Ungerundete Note") }}: {{ userGradeRounded2Places }}.
<a
:href="$t('a.wegleitungUkUrl')"
target="_blank"
rel="noopener noreferrer"
class="underline"
>
{{ $t("a.Wegleitung üK") }}
</a>
</div>
</section>
<section v-else class="py-2">
{{ $t("a.competenceCertificateNoUserPoints") }}
</section>
<CompetenceCertificateGrade
:detail-view="detailView"
:competence-certificate="competenceCertificate"
/>
<ItProgress :status-count="progressStatusCount" />
<div>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { CompetenceCertificate } from "@/types";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
import { assignmentsUserPoints, calcCompetenceCertificateGrade } from "./utils";
export interface Props {
detailView: boolean;
competenceCertificate: CompetenceCertificate;
compactView?: boolean;
label?: string;
}
const props = withDefaults(defineProps<Props>(), {
compactView: false,
});
const userGrade = computed(() => {
return calcCompetenceCertificateGrade(props.competenceCertificate.assignments, true);
});
const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(props.competenceCertificate.assignments);
});
</script>
<template>
<section v-if="userPointsEvaluatedAssignments > 0">
<div class="flex items-center">
<div
class="py-4"
:class="detailView ? 'heading-1' : compactView ? 'heading-3' : 'heading-2'"
:data-cy="`certificate-${competenceCertificate.slug}-grade`"
>
{{ label ?? $t("a.Note") }}: {{ userGrade }}
</div>
</div>
</section>
<section v-else class="py-2">
{{ $t("a.competenceCertificateNoUserPoints") }}
</section>
</template>

View File

@ -22,7 +22,8 @@ const { id: currentUserId } = useUserStore();
const { competenceCertificates } = useAllCompetenceCertificates(
props.userId ?? currentUserId,
props.courseSlug
props.courseSlug,
true
);
const assignments = computed(() => {

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { useAllCompetenceCertificates } from "@/composables";
import { useUserStore } from "@/stores/user";
import CompetenceCertificateComponent from "./CompetenceCertificateComponent.vue";
import CompetenceCertificateGrade from "./CompetenceCertificateGrade.vue";
export interface Props {
courseSlug: string;
}
const props = defineProps<Props>();
const { id: currentUserId } = useUserStore();
const { competenceCertificates } = useAllCompetenceCertificates(
currentUserId,
props.courseSlug
);
</script>
<template>
<div class="bg-gray-200">
<div class="container-large">
<h1>Certificate Overview</h1>
<div v-for="certificate in competenceCertificates" :key="certificate.id">
<CompetenceCertificateGrade
:label="$t('a.Note')"
:compact-view="true"
:detail-view="false"
:competence-certificate="certificate"
/>
</div>
<CompetenceCertificateComponent
v-for="certificate in competenceCertificates"
:key="certificate.id"
:detail-view="false"
:competence-certificate="certificate"
/>
</div>
</div>
</template>

View File

@ -1,101 +1,67 @@
<script setup lang="ts">
import SubNavigation, { type SubNavEntry } from "@/components/header/SubNavigation.vue";
import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables";
import {
CERTIFICATES_ROUTE,
COMPETENCE_ROUTE,
COMPETENCES_ROUTE,
SELF_EVALUATION_ROUTE,
} from "@/router/names";
import { useTranslation } from "i18next-vue";
import * as log from "loglevel";
import { onMounted } from "vue";
import { useRoute } from "vue-router";
log.debug("CompetenceParentPage created");
const props = defineProps<{
courseSlug: string;
}>();
const route = useRoute();
function routeInOverview() {
return route.path.endsWith("/competence");
}
function routeInCompetenceCertificate() {
return route.path.includes("/certificate");
}
function routeInActionCompetences() {
return route.path.endsWith("/competences");
}
function routeInSelfEvaluationAndFeedback() {
return route.path.endsWith("/self-evaluation-and-feedback");
}
const { t } = useTranslation();
const currentCourseSession = useCurrentCourseSession();
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug);
log.debug("CompetenceParentPage mounted");
});
const competenceRoute = {
name: COMPETENCE_ROUTE,
};
const certificatesRoute = {
name: CERTIFICATES_ROUTE,
};
const selfEvaluationRoute = {
name: SELF_EVALUATION_ROUTE,
};
const competencesRoute = {
name: COMPETENCES_ROUTE,
};
// todo: replace this menu with a real one before going live
const items: SubNavEntry[] = [
{ id: 0, name: t("a.Übersicht"), route: competenceRoute },
...(currentCourseSession.value.course.configuration.enable_competence_certificates
? [
{
id: 1,
name: t("a.Kompetenznachweise"),
route: certificatesRoute,
},
]
: []),
{
id: 2,
name: hasEvaluationFeedback.value
? t("a.Selbst- und Fremdeinschätzungen")
: t("a.Selbsteinschätzungen"),
dataCy: "self-evaluation-and-feedback-navigation-link",
route: selfEvaluationRoute,
},
{ id: 3, name: t("a.Handlungskompetenzen"), route: competencesRoute },
];
</script>
<template>
<div class="bg-gray-200">
<nav class="border-b bg-white px-4 lg:px-8">
<ul class="flex flex-col lg:flex-row">
<li
class="border-t-2 border-t-transparent"
:class="{ 'border-b-2 border-b-blue-900': routeInOverview() }"
>
<router-link :to="`/course/${courseSlug}/competence`" class="block py-3">
{{ $t("a.Übersicht") }}
</router-link>
</li>
<li
v-if="
currentCourseSession.course.configuration.enable_competence_certificates
"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
>
<router-link
:to="`/course/${courseSlug}/competence/certificates`"
class="block py-3"
>
{{ $t("a.Kompetenznachweise") }}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{
'border-b-2 border-b-blue-900': routeInSelfEvaluationAndFeedback(),
}"
>
<router-link
:to="`/course/${courseSlug}/competence/self-evaluation-and-feedback`"
class="block py-3"
data-cy="self-evaluation-and-feedback-navigation-link"
>
{{
hasEvaluationFeedback
? $t("a.Selbst- und Fremdeinschätzungen")
: $t("a.Selbsteinschätzungen")
}}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"
>
<router-link
:to="`/course/${courseSlug}/competence/competences`"
class="block py-3"
>
{{ $t("a.Handlungskompetenzen") }}
</router-link>
</li>
<!-- Add similar logic for other `li` items as you expand the list -->
<li class="ml-6 inline-block lg:ml-12"></li>
</ul>
</nav>
<SubNavigation :items="items" />
<main>
<router-view></router-view>
</main>

View File

@ -23,14 +23,21 @@ export function assignmentsUserPoints(assignments: CompetenceCertificateAssignme
).toFixed(1);
}
/**
* Calculates the grade by summing up all the weighted percentage of points of each assignment.
*
* @param assignments - list of assignments
* @param roundedToHalfGrade - should the grade be rounded?
*/
export function calcCompetenceCertificateGrade(
assignments: CompetenceCertificateAssignment[],
roundedToHalfGrade = true
roundedToHalfGrade: boolean = true
) {
const evaluatedAssignments = assignments.filter(
(a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED"
);
// sum((points_x / max_points) * weight_x)
const adjustedResults = evaluatedAssignments.map((a) => {
return (
((a.completions?.[0]?.evaluation_points_final ?? 0) / a.max_points) *
@ -38,6 +45,7 @@ export function calcCompetenceCertificateGrade(
);
});
// count only assignments with weight
const adjustedAssignmentCount = _.sum(
evaluatedAssignments.map((a) => a.competence_certificate_weight)
);

View File

@ -63,9 +63,14 @@ const itemDetailUrl = (item: AssignmentStatisticsRecordType) => {
<template>
<main>
<div class="mb-10 flex items-center justify-between">
<div class="mb-10 flex flex-wrap items-center justify-between">
<h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3>
<button v-if="true" class="flex" data-cy="export-button" @click="exportData">
<button
v-if="true"
class="flex pt-3 sm:pt-0"
data-cy="export-button"
@click="exportData"
>
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
@ -78,7 +83,7 @@ const itemDetailUrl = (item: AssignmentStatisticsRecordType) => {
>
<template #default="{ item }">
<div
class="flex justify-between"
class="flex flex-wrap justify-between"
:data-cy="`${(item as AssignmentStatisticsRecordType).assignment_title}@${(item as AssignmentStatisticsRecordType).course_session_id}`"
>
<div>
@ -109,7 +114,7 @@ const itemDetailUrl = (item: AssignmentStatisticsRecordType) => {
{{ total((item as AssignmentStatisticsRecordType).metrics) }}
bestanden
</div>
<div v-else>Noch nicht bestätigt</div>
<div v-else>{{ $t("a.Noch nicht bestätigt") }}</div>
<ItProgress
:status-count="
assignmentStats((item as AssignmentStatisticsRecordType).metrics)

View File

@ -64,7 +64,7 @@ async function exportData() {
:items="courseStatistics.attendance_day_presences.records"
>
<template #default="{ item }">
<div class="flex justify-between">
<div class="flex flex-wrap justify-between">
<div>
<h4 class="font-bold">
{{ $t("a.Präsenztag") }}: Circle «{{

View File

@ -25,7 +25,7 @@ const props = defineProps<{
:items="courseStatistics.competences.records"
>
<template #default="{ item }">
<div class="flex justify-between">
<div class="flex flex-wrap justify-between space-y-2 md:space-y-0">
<div>
<h4 class="font-bold">
{{ $t("a.Selbsteinschätzung") }}:

View File

@ -52,7 +52,7 @@ async function exportData() {
:items="courseStatistics.feedback_responses.records"
>
<template #default="{ item }">
<div class="flex justify-between">
<div class="flex flex-wrap justify-between space-y-2 md:space-y-0">
<div>
<h4 class="font-bold">
Feedback: Circle «{{ circleMeta(item.circle_id)?.name }}»

View File

@ -242,7 +242,13 @@ watch(
</button>
</div>
<DocumentSection v-if="showDocumentSection" :circle="circle" />
<div v-if="!props.readonly" class="expert mt-8 border p-6">
<div
v-if="
!props.readonly &&
lpQueryResult.course.value?.configuration.enable_learning_mentor
"
class="expert mt-8 border p-6"
>
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
<div
class="mt-4 leading-relaxed"

View File

@ -23,6 +23,7 @@ import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
import RichTextBlock from "./blocks/RichTextBlock.vue";
import VideoBlock from "./blocks/VideoBlock.vue";
import FeedbackBlockAutomobilGewerbe from "./feedback/FeedbackBlockAutomobilGewerbe.vue";
import FeedbackBlockUK from "./feedback/FeedbackBlockUK.vue";
import FeedbackBlockVV from "./feedback/FeedbackBlockVV.vue";
@ -45,6 +46,7 @@ const COMPONENTS: Record<LearningContentContentType, Component> = {
"learnpath.LearningContentDocumentList": DocumentListBlock,
"learnpath.LearningContentFeedbackUK": FeedbackBlockUK,
"learnpath.LearningContentFeedbackVV": FeedbackBlockVV,
"learnpath.LearningContentFeedbackAutomobilGewerbe": FeedbackBlockAutomobilGewerbe,
"learnpath.LearningContentLearningModule": IframeBlock,
"learnpath.LearningContentKnowledgeAssessment": IframeBlock,
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,
@ -58,6 +60,7 @@ const DEFAULT_BLOCK = PlaceholderBlock;
const component = computed(() => {
return COMPONENTS[props.learningContent.content_type] || DEFAULT_BLOCK;
});
console.log("component", component);
function handleFinishedLearningContent() {
circleStore.continueFromLearningContent(

View File

@ -5,14 +5,14 @@ import { graphql } from "@/gql";
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useUserStore } from "@/stores/user";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import type { LearningContentFeedback } from "@/types";
import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router";
import log from "loglevel";
import { computed, onMounted, reactive, ref } from "vue";
const props = defineProps<{
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
content: LearningContentFeedback;
stepLabels: string[];
questionData: any[];
introduction: string;

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
import type { LearningContentFeedback } from "@/types";
import { useTranslation } from "i18next-vue";
const props = defineProps<{
content: LearningContentFeedback;
}>();
const { t } = useTranslation();
const stepLabels = [
t("general.introduction"),
t("feedback.satisfactionLabel"),
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabelVV"),
t("feedback.recommendLabelVV"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("general.submission"),
];
const questionData = [
{
modelKey: "satisfaction",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "goal_attainment",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "proficiency",
items: PERCENTAGES,
component: ItRadioGroup,
},
{
modelKey: "would_recommend",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "course_positive_feedback",
component: ItTextarea,
},
{
modelKey: "course_negative_feedback",
component: ItTextarea,
},
];
</script>
<template>
<FeedbackBase
:step-labels="stepLabels"
:question-data="questionData"
:content="props.content"
:introduction="$t('a.feedback.introductionVV')"
:title="$t('Feedback')"
:completion-title="$t('feedback.sendFeedback')"
:completion-description="$t('feedback.completionDescriptionVV')"
:show-avatar="false"
/>
</template>

View File

@ -8,12 +8,12 @@ import {
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import type { LearningContentFeedback } from "@/types";
import { useTranslation } from "i18next-vue";
import { computed } from "vue";
const props = defineProps<{
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
content: LearningContentFeedback;
}>();
const courseSessionDetailResult = useCourseSessionDetailQuery();

View File

@ -7,11 +7,11 @@ import {
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import type { LearningContentFeedback } from "@/types";
import { useTranslation } from "i18next-vue";
const props = defineProps<{
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
content: LearningContentFeedback;
}>();
const { t } = useTranslation();

View File

@ -133,7 +133,7 @@ function render() {
<div class="aspect-square content-center">
<pre hidden>{{ pieData }}</pre>
<pre hidden>{{ render() }}</pre>
<svg :id="svgId" class="h-full min-w-[20px]">
<svg :id="svgId" class="h-full">
<circle :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]" />
<circle :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white" />
</svg>

View File

@ -15,6 +15,7 @@ import { useEntities } from "@/services/entities";
import { getLocalSessionKey } from "@/statistics";
import { type User, useUserStore } from "@/stores/user";
import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone";
import { validatePostalCode } from "@/utils/postalcode";
import { useTranslation } from "i18next-vue";
import log from "loglevel";
import { computed, ref, watch } from "vue";
@ -129,6 +130,8 @@ function validateAddress() {
if (!address.value.postal_code) {
formErrors.value.personal.push(t("a.PLZ"));
} else if (!validatePostalCode(address.value.postal_code)) {
formErrors.value.personal.push(t("a.Postleizahl hat das falsche Format"));
}
if (!address.value.city) {
@ -172,6 +175,8 @@ function validateAddress() {
if (!address.value.organisation_postal_code) {
formErrors.value.company.push(t("a.PLZ"));
} else if (!validatePostalCode(address.value.organisation_postal_code)) {
formErrors.value.personal.push(t("a.Postleizahl hat das falsche Format"));
}
if (!address.value.organisation_city) {

View File

@ -13,6 +13,18 @@ import {
import { addToHistory, setLastNavigationWasPush } from "@/router/history";
import { onboardingRedirect } from "@/router/onboarding";
import { createRouter, createWebHistory } from "vue-router";
import {
ATTENDANCE_ROUTE,
CERTIFICATE_OVERVIEW_ROUTE,
CERTIFICATES_ROUTE,
COCKPIT_ROUTE,
COMPETENCE_ROUTE,
COMPETENCES_ROUTE,
DOCUMENTS_ROUTE,
PERSONAL_PROFILE_ROUTE,
SELF_EVALUATION_ROUTE,
SETTINGS_ROUTE,
} from "./names";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -79,85 +91,284 @@ const router = createRouter({
props: true,
},
{
path: "/course/:courseSlug/media",
path: "/dashboard/certificates/:courseSlug",
component: () =>
import("@/pages/competence/CompetenceCertificateOverviewPage.vue"),
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryParentPage.vue"),
children: [
{
path: "",
component: () => import("@/pages/mediaLibrary/MediaLibraryIndexPage.vue"),
},
{
path: ":categorySlug",
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryCategoryPage.vue"),
},
{
path: ":categorySlug/:contentSlug",
props: true,
component: () => import("@/pages/mediaLibrary/MediaLibraryContentPage.vue"),
},
],
},
{
path: "/course/:courseSlug/competence",
path: "/course/:courseSlug",
props: true,
component: () => import("@/pages/competence/CompetenceParentPage.vue"),
children: [
{
path: "",
path: "media",
component: () => import("@/pages/mediaLibrary/MediaLibraryParentPage.vue"),
props: true,
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
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: "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: "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: "certificates",
name: CERTIFICATE_OVERVIEW_ROUTE,
props: true,
component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"),
import("@/pages/competence/CompetenceCertificateOverviewPage.vue"),
},
{
path: "certificates/:certificateSlug",
path: "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: "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/competence/CompetenceCertificateDetailPage.vue"),
import("@/pages/assignmentEvaluation/AssignmentEvaluationPage.vue"),
props: true,
},
{
name: "selfEvaluationAndFeedback",
path: "self-evaluation-and-feedback",
props: true,
path: "cockpit",
name: "cockpit",
component: () =>
import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"),
},
{
path: "competences",
import("@/pages/cockpit/cockpitPage/CockpitExpertParentPage.vue"),
props: true,
component: () => import("@/pages/competence/ActionCompetenceListPage.vue"),
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/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"),
@ -168,158 +379,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,
@ -389,10 +448,11 @@ const router = createRouter({
{
path: "/profile",
component: () => import("@/pages/personalProfile/PersonalProfilePage.vue"),
name: "personalProfile",
name: PERSONAL_PROFILE_ROUTE,
},
{
path: "/settings",
name: SETTINGS_ROUTE,
component: () => import("@/pages/SettingsPage.vue"),
},
{

View File

@ -0,0 +1,13 @@
export const COMPETENCE_ROUTE = "competence";
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";
export const MENTORS_PARTICIPANTS_ROUTE = "mentorsAndParticipants";
export const MENTOR_OVERVIEW_ROUTE = "learningMentorOverview";
export const LEARN_ROUTE = "learn";
export const CERTIFICATE_OVERVIEW_ROUTE = "certificateOverview";

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

@ -5,14 +5,6 @@ import { directUpload } from "@/services/files";
import dayjs from "dayjs";
import { defineStore } from "pinia";
let logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT || "/";
if (import.meta.env.VITE_OAUTH_API_BASE_URL) {
logoutRedirectUrl = `${
import.meta.env.VITE_OAUTH_API_BASE_URL
}logout/?post_logout_redirect_uri=${window.location.origin}&client_id=iterativ`;
}
const AVAILABLE_LANGUAGES = ["de", "fr", "it"];
export type AvailableLanguages = "de" | "fr" | "it";
@ -150,16 +142,7 @@ export const useUserStore = defineStore({
handleLogout() {
Object.assign(this, initialUserState);
itPost("/api/core/logout/", {}).then(() => {
let redirectUrl;
if (logoutRedirectUrl !== "") {
redirectUrl = logoutRedirectUrl;
} else {
redirectUrl = "/";
}
window.location.href = redirectUrl;
});
window.location.href = `${window.location.origin}/sso/logout`;
},
async fetchUser() {
const data: any = await itGetCached("/api/core/me/");

View File

@ -11,6 +11,7 @@ import type {
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackAutomobilGewerbeObjectType,
LearningContentFeedbackUkObjectType,
LearningContentFeedbackVvObjectType,
LearningContentKnowledgeAssessmentObjectType,
@ -77,6 +78,11 @@ export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
readonly content_type: "learnpath.LearningContentFeedbackUK";
};
export type LearningContentFeedbackAutomobilGewerbe =
LearningContentFeedbackAutomobilGewerbeObjectType & {
readonly content_type: "learnpath.LearningContentFeedbackAutomobilGewerbe";
};
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
readonly content_type: "learnpath.LearningContentLearningModule";
};
@ -109,6 +115,7 @@ export type LearningContent =
| LearningContentEdoniqTest
| LearningContentFeedbackUK
| LearningContentFeedbackVV
| LearningContentFeedbackAutomobilGewerbe
| LearningContentLearningModule
| LearningContentKnowledgeAssessment
| LearningContentMediaLibrary
@ -116,6 +123,11 @@ export type LearningContent =
| LearningContentRichText
| LearningContentVideo;
export type LearningContentFeedback =
| LearningContentFeedbackUK
| LearningContentFeedbackVV
| LearningContentFeedbackAutomobilGewerbe;
export type LearningContentWithCompletion = LearningContent &
Completable & {
continueUrl?: string;

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,6 +1,7 @@
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user";
import { computed } from "vue";
import type { RouteLocationRaw } from "vue-router";
import { useRouteLookups } from "./route";
export function useNavigationAttributes() {
@ -77,3 +78,7 @@ export function useNavigationAttributes() {
hasSessionTitle,
};
}
export const isExternalLink = (route: string | RouteLocationRaw) => {
return typeof route === "string" && route.startsWith("https");
};

View File

@ -0,0 +1,10 @@
export function validatePostalCode(input: string) {
// Remove non-ASCII characters
// eslint-disable-next-line no-control-regex
input = input.replace(/[^\x00-\x7F]/g, "");
if (input.length < 4) {
return false;
}
const regex = /^[0-9]+$/;
return regex.test(input);
}

View File

@ -1,3 +1,4 @@
import { computed } from "vue";
import { useRoute } from "vue-router";
export function useRouteLookups() {
@ -7,11 +8,15 @@ export function useRouteLookups() {
return route.path.startsWith("/course/");
}
const isInCourse = computed(() => inCourse());
function inCockpit() {
const regex = new RegExp("/course/[^/]+/cockpit($|/)");
return regex.test(route.path);
}
const isInCockpit = computed(() => inCockpit());
function inLearningPath() {
const regex = new RegExp("/course/[^/]+/learn($|/)");
return regex.test(route.path);
@ -39,7 +44,9 @@ export function useRouteLookups() {
return {
inMediaLibrary,
isInCourse,
inCockpit,
isInCockpit,
inLearningPath,
inCompetenceProfile,
inLearningMentor,

View File

@ -51,6 +51,7 @@ export function learningContentTypeData(
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };
case "learnpath.LearningContentFeedbackUK":
case "learnpath.LearningContentFeedbackVV":
case "learnpath.LearningContentFeedbackAutomobilGewerbe":
return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" };
case "learnpath.LearningContentPlaceholder":
return {

View File

@ -176,8 +176,12 @@ textarea {
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
}
.nav-item-base {
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white;
}
.nav-item {
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500;
@apply nav-item-base hover:text-sky-500;
}
.nav-item-no-mobile {

View File

@ -26,6 +26,10 @@ export default defineConfig(({ mode }) => {
host: true,
port: 5173,
strictPort: true,
watch: {
// This fixes HMR but leads to schema changes not being taken into account unless the page is refreshed
ignored: ["**/gql/dist/minifiedSchema.json"],
},
},
resolve: {
alias: {

View File

@ -4,6 +4,9 @@
# Run every 6 hours
0 */6 * * * /usr/local/bin/python /app/manage.py simple_dummy_job
# Run every hour at minute 11
0 */11 * * * /usr/local/bin/python /app/manage.py handle_sso_sync_errors
# Run every hour at minute 17
17 * * * * /usr/local/bin/python /app/manage.py edoniq_import_results

View File

@ -26,7 +26,7 @@ describe("selfEvaluation.cy.js", () => {
cy.visit("/course/test-lehrgang/competence");
cy.get('[data-cy="self-evaluation-fail"]').should("have.text", "0");
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "0");
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "4");
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "6");
// learning unit id = 692 also known as:
// Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>>
@ -45,7 +45,7 @@ describe("selfEvaluation.cy.js", () => {
cy.makeSelfEvaluation([true, false]);
cy.url().should(
"include",
"/course/test-lehrgang/competence/self-evaluation-and-feedback"
"/course/test-lehrgang/competence/self-evaluation-and-feedback",
);
// check data again on KompetenzNavi
@ -57,7 +57,7 @@ describe("selfEvaluation.cy.js", () => {
cy.visit("/course/test-lehrgang/competence");
cy.get('[data-cy="self-evaluation-fail"]').should("have.text", "1");
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "1");
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "2");
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "4");
});
it("should be able to make a happy self evaluation", () => {

View File

@ -16,7 +16,7 @@ describe("dashboardSupervisor.cy.js", () => {
describe("with data", () => {
beforeEach(() => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days"
"cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days",
);
login("test-supervisor1@example.com", "test");
cy.visit("/");
@ -28,7 +28,7 @@ describe("dashboardSupervisor.cy.js", () => {
// -> makes sure that the numbers are correct
getDashboardStatistics("assignments.completed").should(
"have.text",
"1"
"1",
);
getDashboardStatistics("assignments.passed").should("have.text", "34%");
});
@ -48,11 +48,11 @@ describe("dashboardSupervisor.cy.js", () => {
it("contains correct numbers", () => {
getDashboardStatistics("attendance.dayCompleted").should(
"have.text",
"1"
"1",
);
getDashboardStatistics("attendance.participantsPresent").should(
"have.text",
"34%"
"34%",
);
});
it("contains correct details link", () => {
@ -70,7 +70,7 @@ describe("dashboardSupervisor.cy.js", () => {
describe("feedback summary box", () => {
it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3");
getDashboardStatistics("feedback.count").should("have.text", "6");
getDashboardStatistics("feedback.count").should("have.text", "9");
});
it("contains correct details link", () => {
clickOnDetailsLink("feedback");
@ -106,7 +106,7 @@ describe("dashboardSupervisor.cy.js", () => {
describe("with deducted points", () => {
beforeEach(() => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 6,6,6,3,3 --assignment-points-deducted 14 --create-edoniq-test-results 19 24 8"
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 6,6,6,3,3 --assignment-points-deducted 14 --create-edoniq-test-results 19 24 8",
);
login("test-supervisor1@example.com", "test");
cy.visit("/");
@ -120,14 +120,14 @@ describe("dashboardSupervisor.cy.js", () => {
// check data on the details page
cy.get(
'[data-cy="dashboard.stats.assignments"] [data-cy="basebox.detailsLink"]'
'[data-cy="dashboard.stats.assignments"] [data-cy="basebox.detailsLink"]',
).click();
cy.get(
'[data-cy="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)@-1"]'
'[data-cy="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)@-1"]',
).should("contain", "0 von 3 bestanden");
cy.get(
'[data-cy="Überprüfen einer Motorfahrzeugs-Versicherungspolice@-1"]'
'[data-cy="Überprüfen einer Motorfahrzeugs-Versicherungspolice@-1"]',
).should("contain", "0 von 3 bestanden");
});
});

View File

@ -30,93 +30,93 @@ describe("feedbackStudent.cy.js", () => {
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get("[data-cy=\"question-1\"]").should(
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
"Zufriedenheit insgesamt",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-4\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get("[data-cy=\"question-2\"]").should(
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
"Zielerreichung insgesamt",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.instructor_competence).to.equal(null);
}
},
);
cy.get("[data-cy=\"radio-3\"]").click();
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get("[data-cy=\"question-3\"]").should(
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-80\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get("[data-cy=\"question-4\"]").should(
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
"Waren die Vorbereitungsaufträge klar und verständlich?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-false\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get("[data-cy=\"question-5\"]").should(
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-2\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-2"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get("[data-cy=\"question-6\"]").should(
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-1\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-1"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get("[data-cy=\"question-7\"]").should(
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"it-textarea-instructor_open_feedback\"]").type(
"Der Kursleiter ist eigentlich ganz nett."
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
"Der Kursleiter ist eigentlich ganz nett.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
@ -124,26 +124,26 @@ describe("feedbackStudent.cy.js", () => {
// step 8
cy.url().should("include", "step=8");
cy.get("[data-cy=\"question-8\"]").should(
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
"Würdest du den Kurs weiterempfehlen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-true\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-true"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 9
cy.url().should("include", "step=9");
cy.get("[data-cy=\"question-9\"]").should(
cy.get('[data-cy="question-9"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
"Was hat dir besonders gut gefallen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"it-textarea-course_positive_feedback\"]").type(
"Ich bin zufrieden mit den meisten Dingen."
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Ich bin zufrieden mit den meisten Dingen.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
@ -151,21 +151,21 @@ describe("feedbackStudent.cy.js", () => {
// step 10
cy.url().should("include", "step=10");
cy.get("[data-cy=\"question-10\"]").should(
cy.get('[data-cy="question-10"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
"Wo siehst du Verbesserungspotential?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"it-textarea-course_negative_feedback\"]").type(
"Ich bin unzufrieden mit einigen Sachen."
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=11");
cy.get("[data-cy=\"sendFeedbackButton\"]").click();
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
@ -173,7 +173,7 @@ describe("feedbackStudent.cy.js", () => {
});
cy.reload();
cy.get(
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-feedback-status\"]"
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-status"]',
).should("have.attr", "aria-checked", "true");
// reopening page should get directly to last step
@ -197,9 +197,9 @@ describe("feedbackStudent.cy.js", () => {
proficiency: 80,
satisfaction: 4,
would_recommend: true,
feedback_type: "uk"
feedback_type: "uk",
});
}
},
);
});
});
@ -219,8 +219,8 @@ describe("feedbackStudent.cy.js", () => {
cy.url().should((url) => {
expect(url).to.match(/\/reisen\/feedback(\?step=0)?$/);
});
cy.get("[data-cy=\"introduction\"]").contains(
"Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können."
cy.get('[data-cy="introduction"]').contains(
"Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können.",
);
cy.wait(200);
@ -230,82 +230,82 @@ describe("feedbackStudent.cy.js", () => {
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get("[data-cy=\"question-1\"]").should(
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
"Zufriedenheit insgesamt",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-4\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get("[data-cy=\"question-2\"]").should(
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
"Zielerreichung insgesamt",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.course_positive_feedback).to.equal(null);
}
},
);
cy.get("[data-cy=\"radio-3\"]").click();
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get("[data-cy=\"question-3\"]").should(
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-80\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get("[data-cy=\"question-4\"]").should(
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
"Waren die Praxisaufträge klar und verständlich?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-false\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get("[data-cy=\"question-5\"]").should(
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
"Würdest du den Circle weiterempfehlen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"radio-false\"]").click();
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get("[data-cy=\"question-6\"]").should(
cy.get('[data-cy="question-6"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
"Was hat dir besonders gut gefallen?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"it-textarea-course_positive_feedback\"]").type(
"Der Circle ist eigentlich ganz nett."
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Der Circle ist eigentlich ganz nett.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
@ -313,21 +313,21 @@ describe("feedbackStudent.cy.js", () => {
// step 7
cy.url().should("include", "step=7");
cy.get("[data-cy=\"question-7\"]").should(
cy.get('[data-cy="question-7"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
"Wo siehst du Verbesserungspotential?",
);
cy.get("[data-cy=\"next-step\"]").should("be.disabled");
cy.get("[data-cy=\"it-textarea-course_negative_feedback\"]").type(
"Ich bin unzufrieden mit einigen Sachen."
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=8");
cy.get("[data-cy=\"sendFeedbackButton\"]").click();
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
@ -335,7 +335,7 @@ describe("feedbackStudent.cy.js", () => {
});
cy.reload();
cy.get(
"[data-cy=\"test-lehrgang-lp-circle-reisen-lc-feedback-status\"]"
'[data-cy="test-lehrgang-lp-circle-reisen-lc-feedback-status"]',
).should("have.attr", "aria-checked", "true");
// reopening page should get directly to last step
@ -354,9 +354,155 @@ describe("feedbackStudent.cy.js", () => {
proficiency: 80,
satisfaction: 4,
would_recommend: false,
feedback_type: "vv"
feedback_type: "vv",
});
}
},
);
});
});
describe("Feedback Automobilgewerbe", () => {
beforeEach(() => {
cy.visit("/course/test-lehrgang/learn/automobilgewerbe/feedback");
});
it("can open feedback page", () => {
cy.testLearningContentTitle("Feedback");
cy.testLearningContentSubtitle("Feedback");
});
it("can create feedback by giving answers to all steps", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should((url) => {
expect(url).to.match(/\/automobilgewerbe\/feedback(\?step=0)?$/);
});
cy.get('[data-cy="introduction"]').contains(
"Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.course_positive_feedback).to.equal(null);
},
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="question-4"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="question-5"]').should(
"contain",
"Was hat dir besonders gut gefallen?",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Der Circle ist eigentlich ganz nett.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="question-6"]').should(
"contain",
"Wo siehst du Verbesserungspotential?",
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen.",
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=7");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(
/\/automobilgewerbe#lu-transfer-reflexion-feedback?$/,
);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-automobilgewerbe-lc-feedback-status"]',
).should("have.attr", "aria-checked", "true");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/automobilgewerbe/feedback");
cy.url().should("include", "step=7");
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback: "Der Circle ist eigentlich ganz nett.",
goal_attainment: 3,
proficiency: 80,
satisfaction: 4,
would_recommend: false,
feedback_type: "automobilgewerbe",
});
},
);
});
});

View File

@ -1,4 +1,4 @@
import {EXPERT_COCKPIT_URL, login} from "../helpers";
import { EXPERT_COCKPIT_URL, login } from "../helpers";
describe("feedbackTrainer.cy.js", () => {
beforeEach(() => {
@ -10,7 +10,7 @@ describe("feedbackTrainer.cy.js", () => {
login("test-trainer1@example.com", "test");
cy.visit(EXPERT_COCKPIT_URL);
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]',
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
@ -22,7 +22,7 @@ describe("feedbackTrainer.cy.js", () => {
login("test-trainer1@example.com", "test");
cy.visit(EXPERT_COCKPIT_URL);
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]',
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
@ -30,43 +30,43 @@ describe("feedbackTrainer.cy.js", () => {
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
"Zufriedenheit insgesamt",
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
"Zielerreichung insgesamt",
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
"Waren die Vorbereitungsaufträge klar und verständlich?",
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
);
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
"Würdest du den Kurs weiterempfehlen?",
);
cy.get('[data-cy="question-9"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
"Wo siehst du Verbesserungspotential?",
);
cy.get('[data-cy="question-10"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
"Was hat dir besonders gut gefallen?",
);
cy.get('[data-cy="question-1"]')
@ -142,7 +142,7 @@ describe("feedbackTrainer.cy.js", () => {
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]'
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]',
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
@ -150,31 +150,31 @@ describe("feedbackTrainer.cy.js", () => {
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
"Zufriedenheit insgesamt",
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
"Zielerreichung insgesamt",
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
"Waren die Praxisaufträge klar und verständlich?",
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
"Würdest du den Circle weiterempfehlen?",
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
"Wo siehst du Verbesserungspotential?",
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
"Was hat dir besonders gut gefallen?",
);
cy.get('[data-cy="question-1"]')
@ -228,4 +228,87 @@ describe("feedbackTrainer.cy.js", () => {
.should("contain", "Die Präsentation war super");
});
});
describe("FeedbackAutomobilGewerbe", function () {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Automobilgewerbe"]').click();
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-automobilgewerbe-lc-feedback"]',
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt",
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt",
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?",
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Wo siehst du Verbesserungspotential?",
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Was hat dir besonders gut gefallen?",
);
cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.3");
cy.get('[data-cy="question-2"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-40%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-5"]')
.should("contain", "Nichts Schlechtes")
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-6"]')
.should("contain", "Nur Gutes.")
.should(
"contain",
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
)
.should("contain", "Die Präsentation war super");
});
});
});

View File

@ -8,57 +8,61 @@ describe("learningPath.cy.js", () => {
});
it("can open learningPath page", () => {
cy.get("[data-cy=\"learning-path-title\"]").should(
cy.get('[data-cy="learning-path-title"]').should(
"contain",
"Test Lehrgang"
"Test Lehrgang",
);
});
it("can click on circle to open it", () => {
cy.get("[data-cy=\"circle-Fahrzeug\"]").click({ force: true });
cy.get('[data-cy="circle-Fahrzeug"]').click({ force: true });
cy.url().should("include", "/course/test-lehrgang/learn/fahrzeug");
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
});
it("switch between list and path view", () => {
cy.get("[data-cy=\"lp-path-view\"]").should("be.visible");
cy.get("[data-cy=\"view-switch\"]").click();
cy.get("[data-cy=\"lp-list-view\"]").should("be.visible");
cy.get("[data-cy=\"view-switch\"]").click();
cy.get("[data-cy=\"lp-path-view\"]").should("be.visible");
cy.get('[data-cy="lp-path-view"]').should("be.visible");
cy.get('[data-cy="view-switch"]').click();
cy.get('[data-cy="lp-list-view"]').should("be.visible");
cy.get('[data-cy="view-switch"]').click();
cy.get('[data-cy="lp-path-view"]').should("be.visible");
});
it("weiter gehts button will open next circle", () => {
// first click will open first circle
cy.get("[data-cy=\"lp-continue-button\"]")
cy.get('[data-cy="lp-continue-button"]')
.filter(":visible")
.should("contain", "Los geht's")
.click();
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
cy.get("[data-cy=\"back-to-learning-path-button\"]").click();
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get('[data-cy="back-to-learning-path-button"]').click();
// mark a learning content in second circle
cy.get("[data-cy=\"circle-Reisen\"]").click({ force: true });
cy.get("[data-cy=\"ls-continue-button\"]").click();
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
cy.get("[data-cy=\"back-to-learning-path-button\"]").click();
cy.get('[data-cy="circle-Reisen"]').click({ force: true });
cy.get('[data-cy="ls-continue-button"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="back-to-learning-path-button"]').click();
// click on continue should go to unit-test-circle
cy.get("[data-cy=\"lp-continue-button\"]")
cy.get('[data-cy="lp-continue-button"]')
.filter(":visible")
.should("contain", "Weiter geht's")
.click();
cy.get("[data-cy=\"circle-title\"]").should("contain", "Reisen");
cy.get('[data-cy="circle-title"]').should("contain", "Reisen");
});
it("checks contents", () => {
cy.get("[data-cy=\"lp-topic\"]").should("have.length", 2);
cy.get("[data-cy=\"lp-topic\"]").first().should("contain", "Circle ÜK");
cy.get("[data-cy=\"lp-topic\"]").eq(1).should("contain", "Circle VV");
cy.get('[data-cy="lp-topic"]').should("have.length", 3);
cy.get('[data-cy="lp-topic"]').first().should("contain", "Circle ÜK");
cy.get('[data-cy="lp-topic"]').eq(1).should("contain", "Circle VV");
cy.get('[data-cy="lp-topic"]')
.eq(2)
.should("contain", "Circle Automobilgewerbe");
cy.get(".cy-lp-circle").should("have.length", 2);
cy.get(".cy-lp-circle").should("have.length", 3);
cy.get(".cy-lp-circle").first().should("contain", "Fahrzeug");
cy.get(".cy-lp-circle").eq(1).should("contain", "Reisen");
cy.get(".cy-lp-circle").eq(2).should("contain", "Automobilgewerbe");
});
});

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");
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,24 @@
import json
import os
import sys
import django
from django.db import transaction
sys.path.append("../server")
os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
from vbv_lernwelt.core.models import User
# Get the user whose password you want to use as the reference
reference_user = User.objects.get(email='axel.manderbach@lernetz.ch')
reference_user.set_password('test')
reference_user.save()
# Update the password for all users
with transaction.atomic():
User.objects.update(password=reference_user.password)

View File

@ -639,6 +639,8 @@ OAUTH_SIGNIN_REDIRECT_URI = env(
"OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback"
)
OAUTH_LOGOUT_REDIRECT_URI = env("OAUTH_LOGOUT_REDIRECT_URI", default="/")
OAUTH_SIGNIN_URL = env("OAUTH_SIGNIN_URL", default="")
OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv")
OAUTH_SIGNIN_ADMIN_CLIENT_ID = env("OAUTH_SIGNIN_ADMIN_CLIENT_ID", default="")

View File

@ -29,7 +29,6 @@ from vbv_lernwelt.core.views import (
rate_limit_exceeded_view,
vue_home,
vue_login,
vue_logout,
)
from vbv_lernwelt.course.views import (
course_page_api_view,
@ -124,7 +123,6 @@ urlpatterns = [
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
name='vue_login'),
re_path(r'api/core/logout/$', vue_logout, name='vue_logout'),
# notifications
re_path(r'^notifications/', include(notifications.urls, namespace='notifications')),
@ -241,7 +239,7 @@ urlpatterns = [
# testing and debug
path('server/raise_error/',
user_passes_test(lambda u: u.is_superuser, login_url='/login/')(
raise_example_error) ),
raise_example_error)),
path("server/checkratelimit/", check_rate_limit),
]

Some files were not shown because too many files have changed in this diff Show More