Merged in feature/VBV-291-kn-frontend-teilnehmer (pull request #56)
VBV-291 Auftrag Frontend * Fixes in SubmissionView * Change closing button tag * Delete client cypress folder * Add eslint cypress plugin * Add Cypress tests * Reformat de.json * Fix type errors * Fix cypress tests * Add cypress commands * Disable assignment task inputs after submission
This commit is contained in:
parent
f1ab753515
commit
e4b8d7c301
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"name": "Using fixtures to represent data"
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************
|
|
||||||
// This example commands.ts shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
||||||
//
|
|
||||||
// declare global {
|
|
||||||
// namespace Cypress {
|
|
||||||
// interface Chainable {
|
|
||||||
// login(email: string, password: string): Chainable<void>
|
|
||||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
|
||||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
|
||||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
||||||
<title>Components App</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div data-cy-root></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
// ***********************************************************
|
|
||||||
// This example support/component.ts is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "../../tailwind.css";
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
|
|
||||||
import { setupI18n } from "@/i18n.ts";
|
|
||||||
import router from "@/router";
|
|
||||||
import { mount } from "cypress/vue";
|
|
||||||
import { createPinia } from "pinia";
|
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
|
||||||
// your custom command.
|
|
||||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
|
||||||
// with a <reference path="./component" /> at the top of your spec.
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
mount: typeof mount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cypress.Commands.add("mount", (component, options = {}) => {
|
|
||||||
options.global = options.global || {};
|
|
||||||
options.global.plugins = options.global.plugins || [];
|
|
||||||
options.global.plugins.push(setupI18n());
|
|
||||||
options.global.plugins.push(createPinia());
|
|
||||||
|
|
||||||
if (!options.router) {
|
|
||||||
options.router = router;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mount(component, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Example use:
|
|
||||||
// cy.mount(MyComponent)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["es5", "dom"],
|
|
||||||
"target": "es5",
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts"]
|
|
||||||
}
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"eslint": "8.37",
|
"eslint": "8.37",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-cypress": "^2.13.3",
|
||||||
"eslint-plugin-storybook": "^0.6.11",
|
"eslint-plugin-storybook": "^0.6.11",
|
||||||
"eslint-plugin-vue": "^9.4.0",
|
"eslint-plugin-vue": "^9.4.0",
|
||||||
"jsdom": "^21.1.1",
|
"jsdom": "^21.1.1",
|
||||||
|
|
@ -10824,6 +10825,18 @@
|
||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-cypress": {
|
||||||
|
"version": "2.13.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz",
|
||||||
|
"integrity": "sha512-nAPjZE5WopCsgJwl3vHm5iafpV+ZRO76Z9hMyRygWhmg5ODXDPd+9MaPl7kdJ2azj+sO87H3P1PRnggIrz848g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"globals": "^11.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">= 3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||||
|
|
@ -27863,6 +27876,15 @@
|
||||||
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
|
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"eslint-plugin-cypress": {
|
||||||
|
"version": "2.13.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz",
|
||||||
|
"integrity": "sha512-nAPjZE5WopCsgJwl3vHm5iafpV+ZRO76Z9hMyRygWhmg5ODXDPd+9MaPl7kdJ2azj+sO87H3P1PRnggIrz848g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"globals": "^11.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eslint-plugin-prettier": {
|
"eslint-plugin-prettier": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"eslint": "8.37",
|
"eslint": "8.37",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-cypress": "^2.13.3",
|
||||||
"eslint-plugin-storybook": "^0.6.11",
|
"eslint-plugin-storybook": "^0.6.11",
|
||||||
"eslint-plugin-vue": "^9.4.0",
|
"eslint-plugin-vue": "^9.4.0",
|
||||||
"jsdom": "^21.1.1",
|
"jsdom": "^21.1.1",
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
:show-exit-button="stepNo + 1 === numSteps"
|
:show-exit-button="stepNo + 1 === numSteps"
|
||||||
:current-step="stepNo"
|
:current-step="stepNo"
|
||||||
:steps-count="numSteps"
|
:steps-count="numSteps"
|
||||||
:start-badge-text="$t('feedback.introduction')"
|
:start-badge-text="$t('general.introduction')"
|
||||||
:end-badge-text="$t('feedback.submission')"
|
:end-badge-text="$t('general.submission')"
|
||||||
@previous="previousStep()"
|
@previous="previousStep()"
|
||||||
@next="nextStep()"
|
@next="nextStep()"
|
||||||
>
|
>
|
||||||
|
|
@ -141,7 +141,7 @@ const title = computed(
|
||||||
);
|
);
|
||||||
|
|
||||||
const stepLabels = [
|
const stepLabels = [
|
||||||
t("feedback.introduction"),
|
t("general.introduction"),
|
||||||
t("feedback.recommendLabel"),
|
t("feedback.recommendLabel"),
|
||||||
t("feedback.satisfactionLabel"),
|
t("feedback.satisfactionLabel"),
|
||||||
t("feedback.goalAttainmentLabel"),
|
t("feedback.goalAttainmentLabel"),
|
||||||
|
|
@ -152,7 +152,7 @@ const stepLabels = [
|
||||||
t("feedback.instructorOpenFeedbackLabel"),
|
t("feedback.instructorOpenFeedbackLabel"),
|
||||||
t("feedback.courseNegativeFeedbackLabel"),
|
t("feedback.courseNegativeFeedbackLabel"),
|
||||||
t("feedback.coursePositiveFeedbackLabel"),
|
t("feedback.coursePositiveFeedbackLabel"),
|
||||||
t("feedback.submission"),
|
t("general.submission"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const numSteps = stepLabels.length;
|
const numSteps = stepLabels.length;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { RadioGroup, RadioGroupLabel, RadioGroupOption } from "@headlessui/vue";
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: any;
|
modelValue: any;
|
||||||
items: RadioItem<any>[];
|
items: RadioItem<any>[];
|
||||||
label?: string;
|
label: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
|
||||||
|
const meta: Meta<typeof ItSuccessAlert> = {
|
||||||
|
title: "VBV/SuccessAlert",
|
||||||
|
component: ItSuccessAlert,
|
||||||
|
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ItSuccessAlert>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
text: "Deiner Praxisauftrag wurde abgegeben",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
text: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center space-x-2 bg-green-200 px-6">
|
||||||
|
<it-icon-check class="it-icon"></it-icon-check>
|
||||||
|
<p class="text-large py-4">{{ text }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
<textarea
|
<textarea
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
class="h-40 w-full border-gray-500"
|
class="h-40 w-full border-gray-500"
|
||||||
|
:data-cy="`it-textarea-${cyKey}`"
|
||||||
|
:disabled="disabled"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -12,11 +14,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
label?: string;
|
label: string | undefined;
|
||||||
|
cyKey?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
|
cyKey: "",
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
{
|
{
|
||||||
|
"assignment": {
|
||||||
|
"acceptConditionsDisclaimer": "Bedingungen akzeptieren und Ergebnisse abgeben",
|
||||||
|
"assessmentDocumentDisclaimer": "Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsinstrument bewertet:",
|
||||||
|
"assessmentTitle": "Bewertung",
|
||||||
|
"assignmentSubmitted": "Du hast deine Ergebnisse erfolgreich abgegeben.",
|
||||||
|
"confirmSubmitPerson": "Hiermit bestätige ich, dass die folgende Person meine Ergebnisse bewerten soll.",
|
||||||
|
"confirmSubmitResults": "Hiermit bestätige ich, dass ich die Zusammenfassung meiner Ergebnisse überprüft habe und so abgeben will.",
|
||||||
|
"dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am {date} um {time} Uhr ein.",
|
||||||
|
"dueDateNotSet": "Keine Abgabedaten wurden erfasst für diese Durchführung",
|
||||||
|
"dueDateSubmission": "Einreichungstermin: {date}",
|
||||||
|
"dueDateTitle": "Abgabetermin",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"effortTitle": "Zeitaufwand",
|
||||||
|
"initialSituationTitle": "Ausgangslage",
|
||||||
|
"lastChangesNotSaved": "Die letzte Änderung konnte nicht gespeichert werden.",
|
||||||
|
"performanceObjectivesTitle": "Leistungsziele",
|
||||||
|
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
|
||||||
|
"submissionNotificationDisclaimer": "{name} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
|
||||||
|
"submitAssignment": "Ergebnisse abgeben",
|
||||||
|
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
|
||||||
|
"taskDefinitionTitle": "Aufgabenstellung"
|
||||||
|
},
|
||||||
"circlePage": {
|
"circlePage": {
|
||||||
"circleContentBoxTitle": "Das lernst du in diesem Circle.",
|
"circleContentBoxTitle": "Das lernst du in diesem Circle.",
|
||||||
"contactExpertButton": "Fachexpertin kontaktieren",
|
"contactExpertButton": "Fachexpertin kontaktieren",
|
||||||
|
|
@ -70,7 +92,6 @@
|
||||||
"instructorOpenFeedbackLabel": "Was ich dem Kursleiter sonst noch sagen wollte:",
|
"instructorOpenFeedbackLabel": "Was ich dem Kursleiter sonst noch sagen wollte:",
|
||||||
"instructorRespectLabel": "Fragen und Anregungen der Kursteilnehmenden wurden ernst genommen und aufgegriffen.",
|
"instructorRespectLabel": "Fragen und Anregungen der Kursteilnehmenden wurden ernst genommen und aufgegriffen.",
|
||||||
"intro": "{name}, dein/e Trainer/-in, bittet dich, ihm/ihr Feedback zu geben. Das ist freiwillig, würde aber ihm/ihr helfen, deine Lernerlebniss zu verbessern.",
|
"intro": "{name}, dein/e Trainer/-in, bittet dich, ihm/ihr Feedback zu geben. Das ist freiwillig, würde aber ihm/ihr helfen, deine Lernerlebniss zu verbessern.",
|
||||||
"introduction": "Einleitung",
|
|
||||||
"materialsRatingLabel": "Falls ja: Wie beurteilen Sie die Vorbereitungsunterlagen (z.B. eLearning)?",
|
"materialsRatingLabel": "Falls ja: Wie beurteilen Sie die Vorbereitungsunterlagen (z.B. eLearning)?",
|
||||||
"noFeedbacks": "Es wurden noch keine Feedbacks abgegeben",
|
"noFeedbacks": "Es wurden noch keine Feedbacks abgegeben",
|
||||||
"proficiencyLabel": "Wie beurteilen Sie Ihre Sicherheit bezüglichen den Themen nach dem Kurs?",
|
"proficiencyLabel": "Wie beurteilen Sie Ihre Sicherheit bezüglichen den Themen nach dem Kurs?",
|
||||||
|
|
@ -81,7 +102,6 @@
|
||||||
"sendFeedback": "Feedback abschicken",
|
"sendFeedback": "Feedback abschicken",
|
||||||
"sentByUsers": "Von {count} Teilnehmern ausgefüllt",
|
"sentByUsers": "Von {count} Teilnehmern ausgefüllt",
|
||||||
"showDetails": "Details anzeigen",
|
"showDetails": "Details anzeigen",
|
||||||
"submission": "Abgabe",
|
|
||||||
"unhappy": "Unzufrieden",
|
"unhappy": "Unzufrieden",
|
||||||
"veryHappy": "Sehr zufrieden",
|
"veryHappy": "Sehr zufrieden",
|
||||||
"veryUnhappy": "Sehr unzufrieden"
|
"veryUnhappy": "Sehr unzufrieden"
|
||||||
|
|
@ -99,9 +119,11 @@
|
||||||
"backToLearningPath": "zurück zum Lernpfad",
|
"backToLearningPath": "zurück zum Lernpfad",
|
||||||
"certificate": "Zertifikat | Zertifikate",
|
"certificate": "Zertifikat | Zertifikate",
|
||||||
"circles": "Circles",
|
"circles": "Circles",
|
||||||
|
"close": "Schliessen",
|
||||||
"exam": "Prüfung | Prüfungen",
|
"exam": "Prüfung | Prüfungen",
|
||||||
"examResult": "Prüfungsresultat | Prüfungsresultate",
|
"examResult": "Prüfungsresultat | Prüfungsresultate",
|
||||||
"feedback": "Feedback | Feedbacks",
|
"feedback": "Feedback | Feedbacks",
|
||||||
|
"introduction": "Einleitung",
|
||||||
"learningPath": "Lernpfad",
|
"learningPath": "Lernpfad",
|
||||||
"learningSequence": "Lernsequenz",
|
"learningSequence": "Lernsequenz",
|
||||||
"learningUnit": "Lerneinheit",
|
"learningUnit": "Lerneinheit",
|
||||||
|
|
@ -117,6 +139,7 @@
|
||||||
"show": "Anschauen",
|
"show": "Anschauen",
|
||||||
"showAll": "Alle anschauen",
|
"showAll": "Alle anschauen",
|
||||||
"start": "Los geht's",
|
"start": "Los geht's",
|
||||||
|
"submission": "Abgabe",
|
||||||
"title": "myVBV",
|
"title": "myVBV",
|
||||||
"transferTask": "Transferauftrag | Transferaufträge",
|
"transferTask": "Transferauftrag | Transferaufträge",
|
||||||
"yes": "Ja"
|
"yes": "Ja"
|
||||||
|
|
@ -126,7 +149,7 @@
|
||||||
"fr": "Französisch"
|
"fr": "Französisch"
|
||||||
},
|
},
|
||||||
"learningContent": {
|
"learningContent": {
|
||||||
"completeAndContinue": "Als erledigt markieren"
|
"markAsDone": "Als erledigt markieren"
|
||||||
},
|
},
|
||||||
"learningPathPage": {
|
"learningPathPage": {
|
||||||
"currentCircle": "Aktueller Circle",
|
"currentCircle": "Aktueller Circle",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Assignment } from "@/types";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assignment: Assignment;
|
||||||
|
dueDate?: Dayjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
dueDate: undefined,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h3 class="mt-8">{{ $t("assignment.initialSituationTitle") }}</h3>
|
||||||
|
<p class="text-large">{{ props.assignment.starting_position }}</p>
|
||||||
|
|
||||||
|
<h3 class="mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
||||||
|
<p class="text-large">
|
||||||
|
{{ $t("assignment.taskDefinition") }}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="task in props.assignment.tasks" :key="task.id">
|
||||||
|
-
|
||||||
|
<span class="text-large underline">{{ task.value.title }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 class="mt-8">{{ $t("assignment.dueDateTitle") }}</h3>
|
||||||
|
<p v-if="props.dueDate" class="text-large">
|
||||||
|
{{
|
||||||
|
$t("assignment.dueDateIntroduction", {
|
||||||
|
date: dueDate!.format("DD.MM.YYYY"),
|
||||||
|
time: dueDate!.format("HH:mm"),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-large">
|
||||||
|
{{ $t("assignment.dueDateNotSet") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-8">{{ $t("assignment.effortTitle") }}</h3>
|
||||||
|
<p class="text-large">{{ props.assignment.effort_required }}</p>
|
||||||
|
|
||||||
|
<h3 class="mt-8 border-b border-gray-500 pb-2">
|
||||||
|
{{ $t("assignment.performanceObjectivesTitle") }}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
v-for="performance_objective in props.assignment.performance_objectives"
|
||||||
|
:key="performance_objective.id"
|
||||||
|
class="text-large border-b border-gray-500 py-4"
|
||||||
|
>
|
||||||
|
{{ performance_objective.value.text }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-8">{{ $t("assignment.assessmentTitle") }}</h3>
|
||||||
|
<p class="text-large">{{ props.assignment.assessment_description }}</p>
|
||||||
|
<a :href="props.assignment.assessment_document_url" class="text-large underline">
|
||||||
|
{{ $t("assignment.showAssessmentDocument") }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserDataText } from "@/stores/assignmentStore";
|
||||||
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
|
import type { AssignmentTask } from "@/types";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "editTask", task: AssignmentTask): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const assignmentStore = useAssignmentStore();
|
||||||
|
|
||||||
|
const completionData = computed(() => assignmentStore.completion_data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-for="task in assignmentStore.assignment?.tasks ?? []"
|
||||||
|
:key="task.id"
|
||||||
|
class="mb-6 border-t border-gray-400"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row justify-between pt-8">
|
||||||
|
<p class="text-sm text-gray-900">{{ task.value.title }}</p>
|
||||||
|
<button
|
||||||
|
class="link whitespace-nowrap pl-2 text-sm"
|
||||||
|
@click="emit('editTask', task)"
|
||||||
|
>
|
||||||
|
{{ $t("assignment.edit") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||||
|
<p class="pt-6 text-base font-bold">{{ taskBlock.value.text }}</p>
|
||||||
|
<p v-if="completionData && taskBlock.id in completionData" class="font-normal">
|
||||||
|
{{ (completionData[taskBlock.id].user_data as UserDataText).text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItButton from "@/components/ui/ItButton.vue";
|
||||||
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
|
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
|
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||||
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import type { Assignment, AssignmentTask } from "@/types";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
|
import log from "loglevel";
|
||||||
|
import { computed, reactive } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assignment: Assignment;
|
||||||
|
courseSessionId: number;
|
||||||
|
dueDate: Dayjs;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "editTask", task: AssignmentTask): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const assignmentStore = useAssignmentStore();
|
||||||
|
const courseSessionStore = useCourseSessionsStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
confirmInput: false,
|
||||||
|
confirmPerson: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const circleExpert = computed(() => {
|
||||||
|
return courseSessionStore.circleExperts[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const circleExpertName = computed(() => {
|
||||||
|
return `${circleExpert.value?.first_name} ${circleExpert.value?.last_name}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onEditTask = (task: AssignmentTask) => {
|
||||||
|
emit("editTask", task);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
||||||
|
if (!courseSessionId) {
|
||||||
|
log.error("Invalid courseSessionId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await assignmentStore.upsertAssignmentCompletion(
|
||||||
|
props.assignment.id,
|
||||||
|
{},
|
||||||
|
courseSessionId,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Could not submit assignment", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="w-full border border-gray-400 p-8">
|
||||||
|
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
||||||
|
{{ $t("assignment.acceptConditionsDisclaimer") }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div v-if="!assignmentStore.submitted">
|
||||||
|
<ItCheckbox
|
||||||
|
class="w-full border-b border-gray-400 py-6"
|
||||||
|
:checkbox-item="{
|
||||||
|
label: $t('assignment.confirmSubmitResults'),
|
||||||
|
value: 'value',
|
||||||
|
checked: state.confirmInput,
|
||||||
|
}"
|
||||||
|
@toggle="state.confirmInput = !state.confirmInput"
|
||||||
|
></ItCheckbox>
|
||||||
|
<div class="w-full border-b border-gray-400 py-6">
|
||||||
|
<ItCheckbox
|
||||||
|
:checkbox-item="{
|
||||||
|
label: $t('assignment.confirmSubmitPerson'),
|
||||||
|
value: 'value',
|
||||||
|
checked: state.confirmPerson,
|
||||||
|
}"
|
||||||
|
@toggle="state.confirmPerson = !state.confirmPerson"
|
||||||
|
></ItCheckbox>
|
||||||
|
<div class="flex flex-row items-center pl-[49px] pt-3">
|
||||||
|
<img
|
||||||
|
alt="Notification icon"
|
||||||
|
class="mr-2 h-[45px] min-w-[45px] rounded-full"
|
||||||
|
:src="circleExpert.avatar_url"
|
||||||
|
/>
|
||||||
|
<p class="text-base font-bold">
|
||||||
|
{{ circleExpertName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- TODO: find way to find user that will do the corrections -->
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
||||||
|
<p>{{ $t("assignment.assessmentDocumentDisclaimer") }}</p>
|
||||||
|
<a :href="props.assignment.assessment_document_url" class="underline">
|
||||||
|
{{ $t("assignment.showAssessmentDocument") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="pt-6">
|
||||||
|
{{ $t("assignment.dueDateSubmission", { date: dueDate.format("DD.MM.YYYY") }) }}
|
||||||
|
</p>
|
||||||
|
<ItButton
|
||||||
|
class="mt-6"
|
||||||
|
variant="primary"
|
||||||
|
size="large"
|
||||||
|
:disabled="!state.confirmInput || !state.confirmPerson"
|
||||||
|
@click="onSubmit"
|
||||||
|
>
|
||||||
|
<p>{{ $t("assignment.submitAssignment") }}</p>
|
||||||
|
</ItButton>
|
||||||
|
</div>
|
||||||
|
<div v-else class="pt-6">
|
||||||
|
<ItSuccessAlert :text="t('assignment.assignmentSubmitted')"></ItSuccessAlert>
|
||||||
|
<p class="pt-6">
|
||||||
|
{{
|
||||||
|
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AssignmentSubmissionResponses
|
||||||
|
@edit-task="onEditTask"
|
||||||
|
></AssignmentSubmissionResponses>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
|
import type {
|
||||||
|
AssignmentCompletionData,
|
||||||
|
BlockId,
|
||||||
|
UserDataConfirmation,
|
||||||
|
UserDataText,
|
||||||
|
} from "@/stores/assignmentStore";
|
||||||
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import type { AssignmentTask } from "@/types";
|
||||||
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { reactive, ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assignmentId: number;
|
||||||
|
task: AssignmentTask;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const lastSaved = ref(dayjs());
|
||||||
|
const lastSaveUnsuccessful = ref(false);
|
||||||
|
|
||||||
|
const checkboxState = reactive({} as Record<BlockId, boolean>);
|
||||||
|
|
||||||
|
const courseSessionStore = useCourseSessionsStore();
|
||||||
|
const assignmentStore = useAssignmentStore();
|
||||||
|
|
||||||
|
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
|
||||||
|
try {
|
||||||
|
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
||||||
|
if (!courseSessionId) {
|
||||||
|
console.error("Invalid courseSessionId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await assignmentStore.upsertAssignmentCompletion(
|
||||||
|
props.assignmentId,
|
||||||
|
completion_data,
|
||||||
|
courseSessionId,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
lastSaved.value = dayjs();
|
||||||
|
lastSaveUnsuccessful.value = false;
|
||||||
|
console.debug("Saved user input");
|
||||||
|
} catch (error) {
|
||||||
|
lastSaveUnsuccessful.value = true;
|
||||||
|
console.error("Could not save user input", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertAssignmentCompletionDebounced = useDebounceFn(
|
||||||
|
upsertAssignmentCompletion,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUpdateText = (id: BlockId, value: string) => {
|
||||||
|
const data: AssignmentCompletionData = {};
|
||||||
|
data[id] = {
|
||||||
|
user_data: {
|
||||||
|
text: value,
|
||||||
|
} as UserDataText,
|
||||||
|
};
|
||||||
|
upsertAssignmentCompletionDebounced(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
||||||
|
const data: AssignmentCompletionData = {};
|
||||||
|
data[id] = {
|
||||||
|
user_data: {
|
||||||
|
confirmation: value,
|
||||||
|
} as UserDataConfirmation,
|
||||||
|
};
|
||||||
|
upsertAssignmentCompletion(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBlockData = (id: BlockId) => {
|
||||||
|
const userData = assignmentStore.getCompletionDataForUserInput(id)?.user_data;
|
||||||
|
if (userData && "text" in userData) {
|
||||||
|
return userData.text;
|
||||||
|
} else if (userData && "confirmation" in userData) {
|
||||||
|
return userData.confirmation;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleCheckbox = (id: BlockId) => {
|
||||||
|
checkboxState[id] = !checkboxState[id];
|
||||||
|
onUpdateConfirmation(id, checkboxState[id]);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col space-y-10">
|
||||||
|
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||||
|
<div v-if="block.type === 'explanation'">
|
||||||
|
<p class="text-large">{{ block.value.text }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="block.type === 'user_confirmation'">
|
||||||
|
<ItCheckbox
|
||||||
|
:checkbox-item="{
|
||||||
|
label: block.value.text,
|
||||||
|
value: `confirmation-${index}`,
|
||||||
|
checked: getBlockData(block.id) as boolean,
|
||||||
|
}"
|
||||||
|
:disabled="assignmentStore.submitted"
|
||||||
|
@toggle="onToggleCheckbox(block.id)"
|
||||||
|
></ItCheckbox>
|
||||||
|
</div>
|
||||||
|
<div v-if="block.type === 'user_text_input'">
|
||||||
|
<p class="text-large pb-4">{{ block.value.text }}</p>
|
||||||
|
<ItTextarea
|
||||||
|
:model-value="(getBlockData(block.id) as string) ?? ''"
|
||||||
|
:cy-key="`user-text-input-${index}`"
|
||||||
|
:disabled="assignmentStore.submitted"
|
||||||
|
label=""
|
||||||
|
@update:model-value="onUpdateText(block.id, $event)"
|
||||||
|
></ItTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="lastSaveUnsuccessful" class="text-red-600">
|
||||||
|
{{ $t("assignment.lastChangesNotSaved") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.task.value.file_submission_required">
|
||||||
|
<p class="text-large">Datei hochladen</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-900">
|
||||||
|
Mögliche Formate: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
|
||||||
|
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
||||||
|
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
||||||
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
|
import type { AssignmentCompletionData } from "@/stores/assignmentStore";
|
||||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import type {
|
import type {
|
||||||
Assignment,
|
Assignment,
|
||||||
|
AssignmentTask,
|
||||||
CourseSessionAssignmentDetails,
|
CourseSessionAssignmentDetails,
|
||||||
LearningContent,
|
LearningContent,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { onMounted, reactive } from "vue";
|
import { computed, onMounted, reactive } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const courseSessionStore = useCourseSessionsStore();
|
||||||
const assignmentStore = useAssignmentStore();
|
const assignmentStore = useAssignmentStore();
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
assignment: Assignment | undefined;
|
assignment: Assignment | undefined;
|
||||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||||
|
assignmentCompletionData: AssignmentCompletionData | undefined;
|
||||||
|
pageIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = reactive({
|
const state: State = reactive({
|
||||||
assignment: undefined,
|
assignment: undefined,
|
||||||
courseSessionAssignmentDetails: undefined,
|
courseSessionAssignmentDetails: undefined,
|
||||||
|
assignmentCompletionData: undefined,
|
||||||
|
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
||||||
|
pageIndex: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -36,29 +51,104 @@ onMounted(async () => {
|
||||||
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
||||||
props.learningContent.id
|
props.learningContent.id
|
||||||
);
|
);
|
||||||
|
state.assignmentCompletionData = await assignmentStore.loadAssignmentCompletion(
|
||||||
|
props.assignmentId,
|
||||||
|
courseSessionId.value
|
||||||
|
);
|
||||||
|
log.debug(state.assignment, state.courseSessionAssignmentDetails);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const numTasks = computed(() => state.assignment?.tasks?.length ?? 0);
|
||||||
|
const numPages = computed(() => numTasks.value + 2);
|
||||||
|
const showPreviousButton = computed(() => state.pageIndex != 0);
|
||||||
|
const showNextButton = computed(() => state.pageIndex + 1 < numPages.value);
|
||||||
|
const showExitButton = computed(() => numPages.value === state.pageIndex + 1);
|
||||||
|
const dueDate = computed(() =>
|
||||||
|
dayjs(state.courseSessionAssignmentDetails?.deadlineDateTimeUtc)
|
||||||
|
);
|
||||||
|
const courseSessionId = computed(
|
||||||
|
() => courseSessionStore.currentCourseSession?.id ?? 0
|
||||||
|
);
|
||||||
|
const currentTask = computed(() => {
|
||||||
|
if (state.pageIndex > 0 && state.pageIndex <= numTasks.value) {
|
||||||
|
return state.assignment?.tasks[state.pageIndex - 1];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
log.debug("handleBack");
|
||||||
|
if (state.pageIndex > 0) {
|
||||||
|
state.pageIndex -= 1;
|
||||||
|
}
|
||||||
|
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
log.debug("handleContinue");
|
||||||
|
if (state.pageIndex + 1 < numPages.value) {
|
||||||
|
state.pageIndex += 1;
|
||||||
|
}
|
||||||
|
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumpToTask = (task: AssignmentTask) => {
|
||||||
|
log.debug("jumpToTask", task);
|
||||||
|
const index = state.assignment?.tasks.findIndex((t) => t.id === task.id);
|
||||||
|
if (index && index >= 0) {
|
||||||
|
state.pageIndex = index + 1;
|
||||||
|
}
|
||||||
|
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
if (0 === state.pageIndex) {
|
||||||
|
return t("general.introduction");
|
||||||
|
} else if (state.pageIndex === numPages.value - 1) {
|
||||||
|
return t("general.submission");
|
||||||
|
}
|
||||||
|
return currentTask?.value?.value.title ?? "Unknown";
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container-medium">
|
<LearningContentMultiLayout
|
||||||
<div v-if="state.assignment" class="lg:mt-12">
|
:current-step="state.pageIndex"
|
||||||
<h2>Einleitung</h2>
|
:subtitle="state.assignment?.title ?? ''"
|
||||||
|
:title="getTitle()"
|
||||||
<h3 class="mt-8">Ausgangslage</h3>
|
learning-content-type="assignment"
|
||||||
<p>{{ state.assignment.starting_position }}</p>
|
:steps-count="numPages"
|
||||||
|
:show-next-button="showNextButton"
|
||||||
<h3 class="mt-8">Abgabetermin</h3>
|
:show-exit-button="showExitButton"
|
||||||
<p v-if="state.courseSessionAssignmentDetails">
|
:show-start-button="false"
|
||||||
{{ state.courseSessionAssignmentDetails }}
|
:show-previous-button="showPreviousButton"
|
||||||
</p>
|
start-badge-text="Einleitung"
|
||||||
<p v-else>Keine Abgabedaten erfasst für diese Durchführung</p>
|
end-badge-text="Abgabe"
|
||||||
|
close-button-variant="close"
|
||||||
<pre class="mt-16">
|
@previous="handleBack()"
|
||||||
{{ state.assignment }}
|
@next="handleContinue()"
|
||||||
</pre>
|
>
|
||||||
</div>
|
<div>
|
||||||
|
<AssignmentIntroductionView
|
||||||
|
v-if="state.pageIndex === 0 && state.assignment"
|
||||||
|
:due-date="dueDate"
|
||||||
|
:assignment="state.assignment!"
|
||||||
|
></AssignmentIntroductionView>
|
||||||
|
<AssignmentTaskView
|
||||||
|
v-if="currentTask"
|
||||||
|
:task="currentTask"
|
||||||
|
:assignment-id="props.assignmentId"
|
||||||
|
></AssignmentTaskView>
|
||||||
|
<AssignmentSubmissionView
|
||||||
|
v-if="state.pageIndex + 1 === numPages && state.assignment && courseSessionId"
|
||||||
|
:due-date="dueDate"
|
||||||
|
:assignment="state.assignment!"
|
||||||
|
:course-session-id="courseSessionId!"
|
||||||
|
@edit-task="jumpToTask($event)"
|
||||||
|
></AssignmentSubmissionView>
|
||||||
</div>
|
</div>
|
||||||
|
</LearningContentMultiLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<AssignmentView
|
<AssignmentView
|
||||||
class="container-medium"
|
|
||||||
:assignment-id="props.value.assignment"
|
:assignment-id="props.value.assignment"
|
||||||
:learning-content="props.content"
|
:learning-content="props.content"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
interface Props {
|
export type ClosingButtonVariant = "close" | "mark_as_done";
|
||||||
showStartButton?: boolean;
|
|
||||||
showPreviousButton?: boolean;
|
const props = defineProps<{
|
||||||
showNextButton?: boolean;
|
showStartButton: boolean;
|
||||||
showExitButton?: boolean;
|
showPreviousButton: boolean;
|
||||||
|
showNextButton: boolean;
|
||||||
|
showExitButton: boolean;
|
||||||
|
closingButtonVariant: ClosingButtonVariant;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/return-in-computed-property
|
||||||
|
const closingButtonText = computed(() => {
|
||||||
|
switch (props.closingButtonVariant) {
|
||||||
|
case "close":
|
||||||
|
return t("general.close");
|
||||||
|
case "mark_as_done":
|
||||||
|
return t("learningContent.markAsDone");
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
showStartButton: false,
|
|
||||||
showPreviousButton: false,
|
|
||||||
showNextButton: false,
|
|
||||||
showExitButton: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["start", "previous", "next"]);
|
defineEmits(["start", "previous", "next"]);
|
||||||
|
|
@ -56,8 +66,11 @@ defineEmits(["start", "previous", "next"]);
|
||||||
data-cy="complete-and-continue"
|
data-cy="complete-and-continue"
|
||||||
@click="eventBus.emit('finishedLearningContent', true)"
|
@click="eventBus.emit('finishedLearningContent', true)"
|
||||||
>
|
>
|
||||||
{{ "Als erledigt markieren" }}
|
{{ closingButtonText }}
|
||||||
<it-icon-check class="ml-2"></it-icon-check>
|
<it-icon-check
|
||||||
|
v-if="props.closingButtonVariant == 'mark_as_done'"
|
||||||
|
class="ml-2"
|
||||||
|
></it-icon-check>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Layout for learning contents with multiple steps
|
// Layout for learning contents with multiple steps
|
||||||
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
|
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
|
||||||
|
import type { ClosingButtonVariant } from "@/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue";
|
||||||
import LearningContentFooter from "@/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue";
|
import LearningContentFooter from "@/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue";
|
||||||
import type { LearningContentType } from "@/types";
|
import type { LearningContentType } from "@/types";
|
||||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||||
|
|
@ -17,19 +18,21 @@ interface Props {
|
||||||
stepsCount: number;
|
stepsCount: number;
|
||||||
startBadgeText?: string;
|
startBadgeText?: string;
|
||||||
endBadgeText?: string;
|
endBadgeText?: string;
|
||||||
|
closeButtonVariant?: ClosingButtonVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
title: undefined,
|
title: undefined,
|
||||||
startBadgeText: undefined,
|
startBadgeText: undefined,
|
||||||
endBadgeText: undefined,
|
endBadgeText: undefined,
|
||||||
|
closeButtonVariant: "mark_as_done",
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["previous", "next", "exit"]);
|
const emit = defineEmits(["previous", "next", "exit"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container-medium">
|
<div class="container-large">
|
||||||
<div
|
<div
|
||||||
v-if="props.learningContentType !== 'placeholder'"
|
v-if="props.learningContentType !== 'placeholder'"
|
||||||
class="flex h-min w-min items-center gap-2 rounded-full pb-10"
|
class="flex h-min w-min items-center gap-2 rounded-full pb-10"
|
||||||
|
|
@ -38,12 +41,12 @@ const emit = defineEmits(["previous", "next", "exit"]);
|
||||||
:is="learningContentTypeData(props.learningContentType).icon"
|
:is="learningContentTypeData(props.learningContentType).icon"
|
||||||
class="h-6 w-6 text-gray-900"
|
class="h-6 w-6 text-gray-900"
|
||||||
></component>
|
></component>
|
||||||
<p class="whitespace-nowrap text-gray-900">
|
<p class="whitespace-nowrap text-gray-900" data-cy="lc-subtitle">
|
||||||
{{ props.subtitle }}
|
{{ props.subtitle }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="props.title" class="pb-6 text-3xl" data-cy="ln-title">
|
<h2 v-if="props.title" class="pb-6 text-3xl" data-cy="lc-title">
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<ItNavigationProgress
|
<ItNavigationProgress
|
||||||
|
|
@ -61,6 +64,7 @@ const emit = defineEmits(["previous", "next", "exit"]);
|
||||||
:show-next-button="props.showNextButton"
|
:show-next-button="props.showNextButton"
|
||||||
:show-previous-button="props.showPreviousButton"
|
:show-previous-button="props.showPreviousButton"
|
||||||
:show-exit-button="props.showExitButton"
|
:show-exit-button="props.showExitButton"
|
||||||
|
:closing-button-variant="props.closeButtonVariant"
|
||||||
@previous="emit('previous')"
|
@previous="emit('previous')"
|
||||||
@next="emit('next')"
|
@next="emit('next')"
|
||||||
@start="emit('next')"
|
@start="emit('next')"
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,19 @@ const type = learningContentTypeData(props.learningContentType);
|
||||||
class="flex h-min w-full items-center gap-2 pb-8"
|
class="flex h-min w-full items-center gap-2 pb-8"
|
||||||
>
|
>
|
||||||
<component :is="type.icon" class="h-6 w-6 text-gray-900"></component>
|
<component :is="type.icon" class="h-6 w-6 text-gray-900"></component>
|
||||||
<p class="whitespace-nowrap text-gray-900">
|
<p class="whitespace-nowrap text-gray-900" data-cy="lc-subtitle">
|
||||||
{{ type.title }}
|
{{ type.title }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="props.title" data-cy="ln-title">{{ props.title }}</h2>
|
<h2 v-if="props.title" data-cy="lc-title">{{ props.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<LearningContentFooter></LearningContentFooter>
|
<LearningContentFooter
|
||||||
|
:show-next-button="false"
|
||||||
|
:show-previous-button="false"
|
||||||
|
:show-exit-button="true"
|
||||||
|
:show-start-button="false"
|
||||||
|
:closing-button-variant="'close'"
|
||||||
|
></LearningContentFooter>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,39 @@
|
||||||
import { itGet } from "@/fetchHelpers";
|
import { itGet, itPost } from "@/fetchHelpers";
|
||||||
import type { Assignment } from "@/types";
|
import type { Assignment } from "@/types";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export type AssignmentStoreState = {
|
export type AssignmentStoreState = {
|
||||||
assignment: Assignment | undefined;
|
assignment: Assignment | undefined;
|
||||||
|
completion_data: AssignmentCompletionData;
|
||||||
|
submitted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BlockId = string;
|
||||||
|
|
||||||
|
export interface UserDataText {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDataConfirmation {
|
||||||
|
confirmation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignmentCompletionData {
|
||||||
|
// {
|
||||||
|
// "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
||||||
|
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||||
|
// }
|
||||||
|
[key: BlockId]: { user_data: UserDataText | UserDataConfirmation };
|
||||||
|
}
|
||||||
|
|
||||||
export const useAssignmentStore = defineStore({
|
export const useAssignmentStore = defineStore({
|
||||||
id: "assignmentStore",
|
id: "assignmentStore",
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
assignment: undefined,
|
assignment: undefined,
|
||||||
|
completion_data: {},
|
||||||
|
submitted: false,
|
||||||
} as AssignmentStoreState;
|
} as AssignmentStoreState;
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
|
|
@ -23,9 +45,42 @@ export const useAssignmentStore = defineStore({
|
||||||
if (!assignmentData) {
|
if (!assignmentData) {
|
||||||
throw `No assignment found with: ${assignmentId}`;
|
throw `No assignment found with: ${assignmentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.assignment = assignmentData;
|
this.assignment = assignmentData;
|
||||||
return this.assignment;
|
return this.assignment;
|
||||||
},
|
},
|
||||||
|
async loadAssignmentCompletion(assignmentId: number, courseSessionId: number) {
|
||||||
|
log.debug("load assignment completion", assignmentId, courseSessionId);
|
||||||
|
try {
|
||||||
|
const data = await itGet(`/api/assignment/${assignmentId}/${courseSessionId}/`);
|
||||||
|
this.completion_data = data.completion_data;
|
||||||
|
this.submitted = data.completion_status === "submitted";
|
||||||
|
} catch (e) {
|
||||||
|
log.debug("no completion data found ", e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.completion_data;
|
||||||
|
},
|
||||||
|
getCompletionDataForUserInput(id: BlockId) {
|
||||||
|
return this.completion_data[id];
|
||||||
|
},
|
||||||
|
async upsertAssignmentCompletion(
|
||||||
|
assignmentId: number,
|
||||||
|
completion_data: AssignmentCompletionData,
|
||||||
|
courseSessionId: number,
|
||||||
|
submit: boolean
|
||||||
|
) {
|
||||||
|
const data = {
|
||||||
|
assignment_id: assignmentId,
|
||||||
|
completion_status: submit ? "submitted" : "in_progress",
|
||||||
|
course_session_id: courseSessionId,
|
||||||
|
completion_data: completion_data,
|
||||||
|
};
|
||||||
|
const responseData = await itPost(`/api/assignment/upsert/`, data);
|
||||||
|
if (responseData) {
|
||||||
|
this.completion_data = responseData.completion_data;
|
||||||
|
this.submitted = responseData.completion_status === "submitted";
|
||||||
|
}
|
||||||
|
return responseData;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"plugin:cypress/recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { login } from "./helpers";
|
||||||
|
|
||||||
|
const navigateToAssignment = () => {
|
||||||
|
cy.visit(
|
||||||
|
"/course/überbetriebliche-kurse/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("assignment completion", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.manageCommand("cypress_reset");
|
||||||
|
login("admin", "test");
|
||||||
|
navigateToAssignment();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can open assignment", () => {
|
||||||
|
cy.testLearningContentTitle("Einleitung");
|
||||||
|
cy.testLearningContentSubtitle(
|
||||||
|
"Überprüfen einer Motorfahrzeugs-Versicherungspolice"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can navigate through assignment", () => {
|
||||||
|
// 1 Step forward
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.testLearningContentTitle(
|
||||||
|
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2 Steps forward, 1 step backwards
|
||||||
|
for (let i = 0; i !== 2; i++) {
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
}
|
||||||
|
cy.learningContentMultiLayoutPreviousStep();
|
||||||
|
|
||||||
|
cy.testLearningContentTitle(
|
||||||
|
"Teilaufgabe 2: Kundensituation und Ausgangslage"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can save confirmation", () => {
|
||||||
|
// 1 Step forward
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.testLearningContentTitle(
|
||||||
|
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||||
|
);
|
||||||
|
// Click confirmation
|
||||||
|
cy.get('[data-cy="it-checkbox-confirmation-1"]').click({ force: true });
|
||||||
|
cy.wait(250);
|
||||||
|
cy.reload();
|
||||||
|
// 1 Step forward
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.get('[data-cy="it-checkbox-confirmation-1"]').should("be.checked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can save text", () => {
|
||||||
|
// 2 Steps forward
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.testLearningContentTitle(
|
||||||
|
"Teilaufgabe 2: Kundensituation und Ausgangslage"
|
||||||
|
);
|
||||||
|
// Enter text
|
||||||
|
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||||
|
.clear()
|
||||||
|
.type("Hallovelo");
|
||||||
|
cy.wait(550);
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
// 2 Step forward
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
|
||||||
|
"have.value",
|
||||||
|
"Hallovelo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -34,18 +34,18 @@ describe("circle page", () => {
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
|
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
|
||||||
).click();
|
).click();
|
||||||
cy.get('[data-cy="ln-title"]').should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
cy.get('[data-cy="ln-title"]').should("contain", "Mediathek Fahrzeug");
|
cy.get('[data-cy="lc-title"]').should("contain", "Mediathek Fahrzeug");
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
cy.get('[data-cy="ln-title"]').should("contain", "Vorbereitungsauftrag");
|
cy.get('[data-cy="lc-title"]').should("contain", "Vorbereitungsauftrag");
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
|
|
@ -64,7 +64,7 @@ describe("circle page", () => {
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
|
cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
|
|
||||||
cy.get('[data-cy="ln-title"]').should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
|
|
@ -72,12 +72,12 @@ describe("circle page", () => {
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
|
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
cy.get('[data-cy="ln-title"]').should("contain", "Mediathek Fahrzeug");
|
cy.get('[data-cy="lc-title"]').should("contain", "Mediathek Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open learning content by url", () => {
|
it("can open learning content by url", () => {
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/mediathek-fahrzeug");
|
cy.visit("/course/test-lehrgang/learn/fahrzeug/mediathek-fahrzeug");
|
||||||
cy.get('[data-cy="ln-title"]').should("contain", "Mediathek Fahrzeug");
|
cy.get('[data-cy="lc-title"]').should("contain", "Mediathek Fahrzeug");
|
||||||
|
|
||||||
cy.get('[data-cy="close-learning-content"]').click();
|
cy.get('[data-cy="close-learning-content"]').click();
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||||
|
|
|
||||||
|
|
@ -121,3 +121,19 @@ Cypress.Commands.add('makeSelfEvaluation', (answers) => {
|
||||||
// 'myservice.apps.apiclient.serializers.ApiClientRequestResponseLogSerializer'
|
// 'myservice.apps.apiclient.serializers.ApiClientRequestResponseLogSerializer'
|
||||||
// );
|
// );
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
Cypress.Commands.add("learningContentMultiLayoutNextStep", () => {
|
||||||
|
return cy.get('[data-cy="next-step"]').click({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => {
|
||||||
|
return cy.get('[data-cy="previous-step"]').click({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("testLearningContentTitle", (title) => {
|
||||||
|
return cy.get('[data-cy="lc-title"]').should("contain", title);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("testLearningContentSubtitle", (subtitle) => {
|
||||||
|
return cy.get('[data-cy="lc-subtitle"]').should("contain", subtitle);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue