Merged in feature/VBV-451-anwesenheitskontrolle-frontend (pull request #150)

VBV-451 Anwesenheitskontrolle frontend & neues Cockpit

* Regenerate graphql types after rebase

* Fix grading progress

* Fix cypress tests

* Fix circle selection and add CourseSessionAssignment for Fahrzeug Vorbereitungsauftrag

* Use `LearningContentAssignment` explictly

* Improve type safety without `as`

* Disable feedback details button when no feedback

* Extend submission overview titles after review

* Improve attendance check state handling

* Minor translation/wording fixes


Approved-by: Daniel Egger
This commit is contained in:
Elia Bieri 2023-07-20 16:22:28 +00:00
parent 65d527d894
commit b970597a81
33 changed files with 859 additions and 282 deletions

View File

@ -11078,7 +11078,7 @@
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"dev": true,
"engines": {
"node": ">=0.3.1"
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/diff-sequences": {

View File

@ -18,9 +18,7 @@ const keydown = (e: KeyboardEvent) => {
toggle();
}
};
const input = (e: Event) => {
const target = e.target as HTMLInputElement;
log.debug("input", e.type, target.checked, target.value);
const input = () => {
emit("toggle");
};
</script>

View File

@ -10,6 +10,7 @@ defineProps<{
<template>
<ItRow>
<template #firstRow>
<slot name="leading"></slot>
<img class="mr-2 h-[45px] rounded-full" :src="avatarUrl" />
<p class="text-bold lg:leading-[45px]">{{ name }}</p>
</template>

View File

@ -14,7 +14,9 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
*/
const documents = {
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
"\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n": types.AttendanceCheckMutationDocument,
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationGrade: Float\n $evaluationPoints: Float\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_grade: $evaluationGrade\n evaluation_points: $evaluationPoints\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
};
@ -37,10 +39,18 @@ export function graphql(source: string): unknown;
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n"): (typeof documents)["\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationGrade: Float\n $evaluationPoints: Float\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_grade: $evaluationGrade\n evaluation_points: $evaluationPoints\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n }\n"): (typeof documents)["\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationGrade: Float\n $evaluationPoints: Float\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_grade: $evaluationGrade\n evaluation_points: $evaluationPoints\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n"): (typeof documents)["\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@ -329,6 +329,14 @@ export type SendFeedbackMutationMutationVariables = Exact<{
export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array<string> } | null> | null } | null };
export type AttendanceCheckMutationMutationVariables = Exact<{
attendanceCourseId: Scalars['ID']['input'];
attendanceUserList: Array<InputMaybe<AttendanceUserInputType>> | InputMaybe<AttendanceUserInputType>;
}>;
export type AttendanceCheckMutationMutation = { __typename?: 'Mutation', update_course_session_attendance_course_users?: { __typename?: 'AttendanceCourseUserMutation', course_session_attendance_course?: { __typename?: 'CourseSessionAttendanceCourseType', id: string, attendance_user_list?: Array<{ __typename?: 'AttendanceUserType', user_id: any, first_name?: string | null, last_name?: string | null, email?: string | null, status: AttendanceUserStatus } | null> | null } | null } | null };
export type UpsertAssignmentCompletionMutationVariables = Exact<{
assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']['input'];
@ -343,6 +351,13 @@ export type UpsertAssignmentCompletionMutationVariables = Exact<{
export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: any, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null };
export type AttendanceCheckQueryQueryVariables = Exact<{
courseSessionId: Scalars['ID']['input'];
}>;
export type AttendanceCheckQueryQuery = { __typename?: 'Query', course_session_attendance_course?: { __typename?: 'CourseSessionAttendanceCourseType', id: string, attendance_user_list?: Array<{ __typename?: 'AttendanceUserType', user_id: any, status: AttendanceUserStatus } | null> | null } | null };
export type AssignmentCompletionQueryQueryVariables = Exact<{
assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']['input'];
@ -362,6 +377,8 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: '
export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SendFeedbackInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode<SendFeedbackMutationMutation, SendFeedbackMutationMutationVariables>;
export const AttendanceCheckMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AttendanceCheckMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AttendanceUserInputType"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update_course_session_attendance_course_users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"attendance_user_list"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode<AttendanceCheckMutationMutation, AttendanceCheckMutationMutationVariables>;
export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AssignmentCompletionStatus"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_grade"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]}}]} as unknown as DocumentNode<UpsertAssignmentCompletionMutation, UpsertAssignmentCompletionMutationVariables>;
export const AttendanceCheckQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"attendanceCheckQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<AttendanceCheckQueryQuery, AttendanceCheckQueryQueryVariables>;
export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"assignmentCompletionQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"effort_required"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_description"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_document_url"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_tasks"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"intro_text"}},{"kind":"Field","name":{"kind":"Name","value":"performance_objectives"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"translation_key"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]} as unknown as DocumentNode<AssignmentCompletionQueryQuery, AssignmentCompletionQueryQueryVariables>;
export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CourseQueryQuery, CourseQueryQueryVariables>;

View File

@ -1,10 +1,9 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import schema from "../gql/minifiedSchema.json";
import { devtoolsExchange } from "@urql/devtools";
import { cacheExchange } from "@urql/exchange-graphcache";
import { Client, fetchExchange } from "@urql/vue";
import schema from "../gql/minifiedSchema.json";
import {
AssignmentCompletionMutation,
AssignmentCompletionObjectType,
@ -16,6 +15,9 @@ export const graphqlClient = new Client({
devtoolsExchange,
cacheExchange({
schema: schema,
keys: {
AttendanceUserType: (data) => data?.user_id?.toString() ?? null,
},
updates: {
Mutation: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,5 +1,28 @@
import { graphql } from "@/gql";
export const ATTENDANCE_CHECK_MUTATION = graphql(`
mutation AttendanceCheckMutation(
$attendanceCourseId: ID!
$attendanceUserList: [AttendanceUserInputType]!
) {
update_course_session_attendance_course_users(
id: $attendanceCourseId
attendance_user_list: $attendanceUserList
) {
course_session_attendance_course {
id
attendance_user_list {
user_id
first_name
last_name
email
status
}
}
}
}
`);
export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
mutation UpsertAssignmentCompletion(
$assignmentId: ID!

View File

@ -1,5 +1,17 @@
import { graphql } from "@/gql";
export const ATTENDANCE_CHECK_QUERY = graphql(`
query attendanceCheckQuery($courseSessionId: ID!) {
course_session_attendance_course(id: $courseSessionId) {
id
attendance_user_list {
user_id
status
}
}
}
`);
export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
query assignmentCompletionQuery(
$assignmentId: ID!

View File

@ -36,7 +36,7 @@ export function i18nextInit() {
"7518c269-cbf7-4d25-bc5c-6ceba2a8b74b",
apiKey: import.meta.env.DEV ? import.meta.env.VITE_LOCIZE_API_KEY : undefined,
fallbackLng: "de",
allowedAddOrUpdateHosts: ["localhost"],
allowedAddOrUpdateHosts: ["localhost", "127.0.0.1"],
},
})
);

View File

@ -1,8 +1,21 @@
{
"Anwesenheit Präsenzkurse": "Anwesenheit Präsenzkurse",
"Anwesenheit bestätigen": "Anwesenheit bestätigen",
"Anwesenheit prüfen": "Anwesenheit prüfen",
"Anwesenheitskontrolle Präsenzkurse": "Anwesenheitskontrolle Präsenzkurse",
"Benutzername": "Benutzername",
"Ergebnisse anschauen": "Ergebnisse anschauen",
"Feedback": "Feedback",
"Feedback anschauen": "Feedback anschauen",
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden": [],
"MS Teams öffnen": "MS Teams öffnen",
"Nächste Termine": "Nächste Termine",
"Passwort": "Passwort",
"Status anschauen": "Status anschauen",
"TODO: Nächste Termine": "TODO: Nächste Termine",
"Trainerunterlagen": "Trainerunterlagen",
"Vorbereitungsauftrag": "Vorbereitungsauftrag",
"Wissens - und Verständnisfragen": "Wissens - und Verständnisfragen",
"Zur Zeit sind keine Termine vorhanden": "Zur Zeit sind keine Termine vorhanden",
"assignment": {
"acceptConditionsDisclaimer": "Bedingungen akzeptieren und Ergebnisse abgeben",
@ -243,5 +256,9 @@
},
"settings": {
"emailNotifications": "Email Benachrichtigungen"
}
},
"x von y Bewertungen freigegeben": "{{x}} von {{y}} Bewertungen freigegeben",
"x von y Ergebnisse abgegeben": "{{x}} von {{y}} Ergebnisse abgegeben",
"x von y Feedbacks abgegeben": "{{x}} von {{y}} Feedbacks abgegeben",
"x von y abgeschlossen": "{{x}} von {{y}} abgeschlossen"
}

View File

@ -34,14 +34,19 @@
:ratio="0.2"
/>
<OpenFeedback
v-else-if="openKeys.includes(question.key)"
v-else-if="
openKeys.includes(question.key) && feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
></OpenFeedback>
<HorizontalBarChart
v-else-if="horizontalChartKeys.includes(question.key)"
v-else-if="
horizontalChartKeys.includes(question.key) &&
feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question"

View File

@ -1,20 +1,21 @@
<script setup lang="ts">
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/assignmentsPage/AssignmentSubmissionProgress.vue";
import type { StatusCount } from "@/components/ui/ItProgress.vue";
import type { GradedUser } from "@/services/assignmentService";
import {
findAssignmentDetail,
loadAssignmentCompletionStatusData,
} from "@/services/assignmentService";
import { useCockpitStore } from "@/stores/cockpit";
import type {
AssignmentCompletionStatus,
CourseSession,
CourseSessionUser,
LearningContentAssignment,
} from "@/types";
import dayjs from "dayjs";
import log from "loglevel";
import { computed, onMounted, reactive } from "vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
const props = defineProps<{
courseSession: CourseSession;
@ -29,59 +30,53 @@ log.debug(
const cockpitStore = useCockpitStore();
const state = reactive({
statusByUser: [] as {
userStatus: AssignmentCompletionStatus;
progressStatus: StatusCountKey;
userId: string;
grade: number | null;
}[],
progressStatusCount: {} as StatusCount,
gradedUsers: [] as GradedUser[],
assignmentSubmittedUsers: [] as CourseSessionUser[],
});
onMounted(async () => {
state.statusByUser = await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.courseSession.id
);
const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.courseSession.id
);
state.gradedUsers = gradedUsers;
state.assignmentSubmittedUsers = assignmentSubmittedUsers;
});
function submissionStatusForUser(userId: string) {
return state.statusByUser.find((s) => s.userId === userId);
}
const assignmentDetail = computed(() =>
findAssignmentDetail(props.learningContentAssignment.content_assignment_id)
);
</script>
<template>
<div v-if="state.statusByUser.length">
<div class="text-large font-bold">
{{ props.learningContentAssignment.title }}
<div>
<h2 class="heading-2 font-bold">
{{ learningContentAssignment.title }}
</h2>
<div class="pt-1 underline">
Circle «{{ learningContentAssignment.parentCircle.title }}»
</div>
<div v-if="assignmentDetail">
<span>
Abgabetermin:
{{ $t("Abgabetermin Ergebnisse:") }}
{{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }}
</span>
-
<span>
Freigabetermin:
{{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
</span>
<template v-if="assignmentDetail.evaluation_deadline_start">
<br />
<span v-if="assignmentDetail.evaluation_deadline_start">
{{ $t("Freigabetermin Bewertungen:") }}
{{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
</span>
</template>
</div>
<div>
<a
:href="props.learningContentAssignment.frontend_url"
class="link"
target="_blank"
>
Im Circle anzeigen
</a>
<div v-else>
{{ $t("Keine Auftragsdetails verfügbar.") }}
</div>
<div class="mt-4">
<!-- how to determine assignment-type? how to get AssignmentLearningContent? -->
<AssignmentSubmissionProgress
:course-session="courseSession"
:learning-content-assignment="learningContentAssignment"
@ -102,9 +97,7 @@ const assignmentDetail = computed(() =>
<section class="flex w-full justify-between px-8">
<div
v-if="
['EVALUATION_SUBMITTED'].includes(
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
)
state.gradedUsers.map((gradedUser) => gradedUser.user).includes(csu)
"
class="flex items-center"
>
@ -116,11 +109,7 @@ const assignmentDetail = computed(() =>
<div class="ml-2">Bewertung freigegeben</div>
</div>
<div
v-else-if="
['EVALUATION_IN_PROGRESS', 'SUBMITTED'].includes(
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
)
"
v-else-if="state.assignmentSubmittedUsers.includes(csu)"
class="flex items-center"
>
<div
@ -130,21 +119,27 @@ const assignmentDetail = computed(() =>
</div>
<div class="ml-2">Ergebnisse abgegeben</div>
</div>
<div v-else></div>
<div v-if="submissionStatusForUser(csu.user_id)?.grade">
Note: {{ submissionStatusForUser(csu.user_id)?.grade }}
<div
v-if="
state.gradedUsers.map((gradedUser) => gradedUser.user).includes(csu)
"
>
Note:
{{
state.gradedUsers.find((u) => u.user.user_id === csu.user_id)?.grade
}}
</div>
</section>
</template>
<template #link>
<router-link
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'SUCCESS'"
v-if="state.assignmentSubmittedUsers.includes(csu)"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment_id}/${csu.user_id}`"
class="w-full text-right underline"
data-cy="show-results"
>
Ergebnisse anzeigen
{{ $t("Ergebnisse anzeigen") }}
</router-link>
</template>
</ItPersonRow>

View File

@ -1,64 +0,0 @@
<script setup lang="ts">
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
import type {
AssignmentCompletionStatus,
CourseSession,
LearningContentAssignment,
} from "@/types";
import { countBy } from "lodash";
import log from "loglevel";
import { onMounted, reactive } from "vue";
const props = defineProps<{
courseSession: CourseSession;
learningContentAssignment: LearningContentAssignment;
showTitle: boolean;
}>();
log.debug(
"AssignmentSubmissionProgress created",
props.learningContentAssignment.content_assignment_id
);
const state = reactive({
statusByUser: [] as {
userStatus: AssignmentCompletionStatus;
progressStatus: StatusCountKey;
userId: string;
}[],
progressStatusCount: {} as StatusCount,
});
onMounted(async () => {
state.statusByUser = await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.courseSession.id
);
state.progressStatusCount = countBy(
state.statusByUser,
"progressStatus"
) as StatusCount;
});
</script>
<template>
<div v-if="state.statusByUser.length">
<div v-if="showTitle">
{{ props.learningContentAssignment.title }}
</div>
<div><ItProgress :status-count="state.progressStatusCount" /></div>
<div class="text-gray-900" :class="{ 'text-gray-900': showTitle }">
{{ state.progressStatusCount.SUCCESS || 0 }} von
{{
(state.progressStatusCount.SUCCESS || 0) +
(state.progressStatusCount.UNKNOWN || 0)
}}
Lernenden haben ihre Ergebnisse eingereicht.
</div>
</div>
</template>
<style scoped></style>

View File

@ -1,30 +1,31 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue";
import { calcLearningContentAssignments } from "@/services/assignmentService";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
import { useUserStore } from "@/stores/user";
import { useLearningPathStore } from "@/stores/learningPath";
import { calcLearningContentAssignments } from "@/services/assignmentService";
const props = defineProps<{
courseSlug: string;
assignmentId: string;
}>();
log.debug("AssignmentsPage created", props.courseSlug);
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const courseSession = useCurrentCourseSession();
const userStore = useUserStore();
const learningPathStore = useLearningPathStore();
onMounted(async () => {
log.debug("AssignmentsPage mounted");
});
const learningContentAssignments = computed(() => {
const learningContentAssignment = computed(() => {
return calcLearningContentAssignments(
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
);
).filter((lc) => lc.id.toString() === props.assignmentId)[0];
});
</script>
@ -40,20 +41,13 @@ const learningContentAssignments = computed(() => {
<span>{{ $t("general.back") }}</span>
</router-link>
</nav>
<header>
<h2 class="heading-2 mb-4 flex items-center gap-2">
<it-icon-assignment-large class="h-16 w-16"></it-icon-assignment-large>
<div>Geleitete Fallarbeiten</div>
</h2>
</header>
<main>
<div v-for="lca in learningContentAssignments" :key="lca.id">
<div class="bg-white p-6">
<AssignmentDetails
:course-session="courseSession"
:learning-content-assignment="lca"
/>
</div>
<div class="bg-white p-6">
<AssignmentDetails
v-if="learningContentAssignment"
:course-session="courseSession"
:learning-content-assignment="learningContentAssignment"
/>
</div>
</main>
</div>

View File

@ -0,0 +1,179 @@
<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 { useCurrentCourseSession } from "@/composables";
import type { AttendanceUserStatus } from "@/gql/graphql";
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { useCockpitStore } from "@/stores/cockpit";
import type { DropdownSelectable } from "@/types";
import { useMutation, useQuery } from "@urql/vue";
import dayjs from "dayjs";
import log from "loglevel";
import { computed, reactive, watch } from "vue";
import { useTranslation } from "i18next-vue";
const { t } = useTranslation();
const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession();
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
const attendanceCourses = computed(() => {
return courseSession.value.attendance_courses;
});
const presenceCoursesDropdownOptions = computed(() => {
return attendanceCourses.value.map(
(attendanceCourse) =>
({
id: attendanceCourse.id,
name: `${t("Präsenzkurs")} ${dayjs(attendanceCourse.start).format(
"DD.MM.YYYY"
)}`,
} as DropdownSelectable)
);
});
const attendanceCourseSelected = computed(
() => state.attendanceCourseSelected.id != "-1"
);
const state = reactive({
userPresence: new Map<string, boolean>(),
attendanceCourseSelected: presenceCoursesDropdownOptions.value[0],
disclaimerConfirmed: false,
attendanceSaved: false,
});
const attendanceQuery = useQuery({
query: ATTENDANCE_CHECK_QUERY,
pause: true,
variables: {
courseSessionId: state.attendanceCourseSelected.id.toString(),
},
});
const onSubmit = async () => {
type UserPresence = {
user_id: string;
status: AttendanceUserStatus;
};
const attendanceUserList: UserPresence[] = Array.from(state.userPresence.keys()).map(
(key) => ({
user_id: key,
status: state.userPresence.get(key) ? "PRESENT" : "ABSENT",
})
);
const res = await attendanceMutation.executeMutation({
attendanceCourseId: state.attendanceCourseSelected.id.toString(),
attendanceUserList: attendanceUserList,
});
if (res.error) {
log.error("Could not submit attendance check: ", res.error);
return;
}
state.disclaimerConfirmed = false;
state.attendanceSaved = true;
log.info("Attendance check submitted: ", res);
};
const loadAttendanceData = async () => {
const res = await attendanceQuery.executeQuery();
const attendanceUserList =
res?.data?.value?.course_session_attendance_course?.attendance_user_list ?? [];
for (const user of attendanceUserList) {
if (!user) continue;
state.userPresence.set(user.user_id.toString(), user.status === "PRESENT");
}
if (attendanceUserList.length !== 0) {
state.attendanceSaved = true;
}
};
loadAttendanceData();
watch(state.attendanceCourseSelected, () => {
loadAttendanceData();
});
</script>
<template>
<div class="bg-gray-200">
<div 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>
<div class="pb-4 text-xl font-bold">{{ $t("Anwesenheit Präsenzkurse") }}</div>
<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
:disabled="!attendanceCourseSelected"
:checkbox-item="{
value: true,
checked: state.disclaimerConfirmed,
}"
@toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
></ItCheckbox>
<p class="w-64 pr-4 text-sm">
{{
$t(
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
)
}}
</p>
<button
class="btn-primary"
:disabled="!state.disclaimerConfirmed"
@click="onSubmit"
>
{{ $t("Anwesenheit bestätigen") }}
</button>
</div>
<div v-else class="self-center">
<p class="text-base">
{{ $t("Die Anwesenheit wurde definitiv bestätigt.") }}
</p>
</div>
</div>
<div class="mt-4 flex flex-col bg-white p-6">
<div
v-for="(csu, index) in cockpitStore.courseSessionUsers"
:key="csu.user_id + csu.session_title"
>
<ItPersonRow
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
:class="0 === index ? 'border-none' : ''"
>
<template #leading>
<ItCheckbox
:disabled="!attendanceCourseSelected || state.attendanceSaved"
:checkbox-item="{
value: true,
checked: state.userPresence.get(csu.user_id.toString()) as boolean,
}"
@toggle="
state.userPresence.set(
csu.user_id.toString(),
!state.userPresence.get(csu.user_id.toString())
)
"
></ItCheckbox>
</template>
</ItPersonRow>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,106 @@
<script setup lang="ts">
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
import type {
AssignmentCompletionStatus,
CourseSession,
LearningContentAssignment,
} from "@/types";
import log from "loglevel";
import { onMounted, reactive } from "vue";
const props = defineProps<{
courseSession: CourseSession;
learningContentAssignment: LearningContentAssignment;
showTitle: boolean;
}>();
log.debug(
"AssignmentSubmissionProgress created",
props.learningContentAssignment.content_assignment_id
);
const state = reactive({
statusByUser: [] as {
userStatus: AssignmentCompletionStatus;
progressStatus: StatusCountKey;
userId: number;
}[],
submissionProgressStatusCount: {} as StatusCount,
gradingProgressStatusCount: {} as StatusCount,
});
onMounted(async () => {
const { assignmentSubmittedUsers, gradedUsers, total } =
await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.courseSession.id
);
state.submissionProgressStatusCount = {
SUCCESS: assignmentSubmittedUsers.length,
UNKNOWN: total - assignmentSubmittedUsers.length,
FAIL: 0,
};
state.gradingProgressStatusCount = {
SUCCESS: gradedUsers.length,
UNKNOWN: total - gradedUsers.length,
FAIL: 0,
};
});
const doneCount = (status: StatusCount) => {
return status.SUCCESS || 0;
};
const totalCount = (status: StatusCount) => {
return doneCount(status) + status.UNKNOWN || 0;
};
</script>
<template>
<div>
<div v-if="showTitle">
{{ props.learningContentAssignment.title }}
</div>
<ItProgress :status-count="state.submissionProgressStatusCount" />
<div class="text-gray-900">
<div v-if="props.learningContentAssignment.assignment_type === 'CASEWORK'">
{{
$t("x von y Ergebnisse abgegeben", {
x: doneCount(state.submissionProgressStatusCount),
y: totalCount(state.submissionProgressStatusCount),
})
}}
<br />
{{
$t("x von y Bewertungen freigegeben", {
x: doneCount(state.gradingProgressStatusCount),
y: totalCount(state.gradingProgressStatusCount),
})
}}
</div>
<div
v-else-if="
props.learningContentAssignment.assignment_type === 'PREP_ASSIGNMENT'
"
>
{{
$t("x von y abgeschlossen", {
x: doneCount(state.submissionProgressStatusCount),
y: totalCount(state.submissionProgressStatusCount),
})
}}
</div>
<div v-else-if="props.learningContentAssignment.assignment_type === 'REFLECTION'">
{{
$t("x von y abgeschlossen", {
x: doneCount(state.submissionProgressStatusCount),
y: totalCount(state.submissionProgressStatusCount),
})
}}
</div>
</div>
</div>
</template>

View File

@ -1,57 +0,0 @@
<script setup lang="ts">
import AssignmentSubmissionProgress from "@/pages/cockpit/assignmentsPage/AssignmentSubmissionProgress.vue";
import { calcLearningContentAssignments } from "@/services/assignmentService";
import { useCockpitStore } from "@/stores/cockpit";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types";
import log from "loglevel";
import { computed } from "vue";
const props = defineProps<{
courseSession: CourseSession;
}>();
log.debug("AssignmentsTile created", props.courseSession.id);
const userStore = useUserStore();
const cockpitStore = useCockpitStore();
const learningPathStore = useLearningPathStore();
const learningContentAssignments = computed(() => {
// TODO: filter by selected circle
return calcLearningContentAssignments(
learningPathStore.learningPathForUser(props.courseSession.course.slug, userStore.id)
);
});
</script>
<template>
<div class="bg-white px-6 py-5">
<div v-if="cockpitStore.courseSessionUsers">
<h3 class="heading-3 mb-4 flex items-center gap-2">
<it-icon-assignment-large class="h-16 w-16"></it-icon-assignment-large>
<div>Geleitete Fallarbeiten</div>
</h3>
<div v-for="lca in learningContentAssignments" :key="lca.id" class="mb-4">
<AssignmentSubmissionProgress
:show-title="true"
:course-session="props.courseSession"
:learning-content-assignment="lca"
/>
</div>
<div class="mt-6">
<router-link
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment`"
class="link"
>
Alle anzeigen
</router-link>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import type { Dayjs } from "dayjs";
import type { DueDate } from "@/types";
const courseSession = useCurrentCourseSession();
const dueDates = courseSession.value.due_dates.slice(0, 2);
const formatDate = (date: Dayjs) => {
return date.format("DD.MM.YYYY");
};
const formatDateLine = (dueDate: DueDate) => {
let line = `${formatDate(dueDate.start)} - ${dueDate.learning_content_description}`;
if (dueDate.description.length !== 0) {
line += `: ${dueDate.description}`;
}
return line;
};
</script>
<template>
<div class="flex flex-col space-y-2">
<h3 class="heading-3">{{ $t("Nächste Termine") }}</h3>
<div
v-for="dueDate in dueDates"
:key="dueDate.id"
class="border-t border-gray-500 pt-2"
>
{{ formatDateLine(dueDate) }}
</div>
<div v-if="dueDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
<a class="border-t border-gray-500 pt-8 underline" href="">
{{ $t("dueDates.showAllDueDates") }}
</a>
</div>
</template>

View File

@ -1,11 +1,10 @@
<script setup lang="ts">
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { LearningPath } from "@/services/learningPath";
import { useCurrentCourseSession } from "@/composables";
import AssignmentsTile from "@/pages/cockpit/cockpitPage/AssignmentsTile.vue";
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath";
@ -13,6 +12,7 @@ import { useUserStore } from "@/stores/user";
import groupBy from "lodash/groupBy";
import log from "loglevel";
import { computed } from "vue";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
const props = defineProps<{
courseSlug: string;
@ -60,7 +60,7 @@ const circles = computed(() => {
const selectedCirclesTitles = computed(() => {
return circles.value
.filter((c) => cockpitStore.selectedCircles.includes(c.translation_key))
.map((c) => c.title);
.map((c) => c.title) as string[];
});
function setActiveClasses(translationKey: string) {
@ -73,26 +73,29 @@ function setActiveClasses(translationKey: string) {
<template>
<div class="bg-gray-200">
<div class="container-large">
<div class="mb-9 flex items-center lg:flex-row">
<h1 class="heading-3">{{ $t("general.circles") }}:</h1>
<ul class="ml-4 flex flex-row text-base font-bold leading-7">
<li
v-for="circle in circles"
:key="circle.translation_key"
class="mr-4 last:mr-0"
>
<button
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
:class="setActiveClasses(circle.translation_key)"
@click="cockpitStore.toggleCircleSelection(circle.translation_key)"
<div class="mb-9 flex items-end justify-between">
<h1>Cockpit</h1>
<div class="flex flex-row">
<p class="text-base">{{ $t("general.circles") }}:</p>
<ul class="ml-4 flex flex-row text-base font-bold leading-7">
<li
v-for="circle in circles"
:key="circle.translation_key"
class="mr-4 last:mr-0"
>
{{ circle.title }}
</button>
</li>
</ul>
<button
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
:class="setActiveClasses(circle.translation_key)"
@click="cockpitStore.toggleCircleSelection(circle.translation_key)"
>
{{ circle.title }}
</button>
</li>
</ul>
</div>
</div>
<!-- Status -->
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
<div class="mb-4 grid grid-rows-3 gap-4 lg:grid-cols-3 lg:grid-rows-none">
<div class="flex flex-col justify-between bg-white px-6 py-5">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
@ -112,21 +115,39 @@ function setActiveClasses(translationKey: string) {
</a>
</div>
</div>
<AssignmentsTile :course-session="courseSession" />
<div class="flex flex-col justify-between bg-white p-6">
<div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("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/attendanceCheck`"
class="btn-secondary min-w-min"
>
{{ $t("Anwesenheit prüfen") }}
</router-link>
</div>
</div>
<div class="bg-white p-6">
<CockpitDates></CockpitDates>
</div>
</div>
<!-- Feedback -->
<FeedbackSummary
:selcted-circles="cockpitStore.selectedCircles || []"
:circles="
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
?.circles || []
"
:course-id="courseSession.course.id"
:url="courseSession.course_url || ''"
></FeedbackSummary>
<div>
<SubmissionsOverview
:course-session="courseSession"
:selected-circles="selectedCirclesTitles"
></SubmissionsOverview>
<div class="pt-4">
<!-- progress -->
<div v-if="cockpitStore.courseSessionUsers?.length" class="bg-white p-6">
<div v-if="cockpitStore.courseSessionUsers" class="bg-white p-6">
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul>
<ItPersonRow

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import type { CourseSession } from "@/types";
import log from "loglevel";
import { computed, onMounted, ref } from "vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import { itGet } from "@/fetchHelpers";
import { useCockpitStore } from "@/stores/cockpit";
const props = defineProps<{
courseSession: CourseSession;
circleId: number;
}>();
log.debug("FeedbackSubmissionProgress created");
const cockpitStore = useCockpitStore();
const completeFeedbacks = ref(0);
const numFeedbacks = computed(() => {
return cockpitStore.courseSessionUsers?.length ?? 0;
});
onMounted(async () => {
const data = await itGet(
`/api/core/feedback/${props.courseSession.course.id}/${props.circleId}/`
);
completeFeedbacks.value = data.amount;
});
</script>
<template>
<div>
<ItProgress
:status-count="{
SUCCESS: completeFeedbacks,
UNKNOWN: numFeedbacks - completeFeedbacks,
FAIL: 0,
}"
/>
<div class="text-gray-900">
{{
$t("x von y Feedbacks abgegeben", {
x: completeFeedbacks,
y: numFeedbacks,
})
}}
</div>
</div>
</template>

View File

@ -0,0 +1,163 @@
2
<script setup lang="ts">
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useCockpitStore } from "@/stores/cockpit";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type {
CourseSession,
LearningContent,
LearningContentAssignment,
} from "@/types";
import log from "loglevel";
import { computed } from "vue";
import { useTranslation } from "i18next-vue";
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Submittable {
id: number;
circleName: string;
frontendUrl: string;
title: string;
showDetailsText: string;
detailsLink: string;
content: LearningContent;
}
const props = defineProps<{
courseSession: CourseSession;
selectedCircles: string[];
}>();
log.debug("SubmissionsOverview created", props.courseSession.id);
const userStore = useUserStore();
const cockpitStore = useCockpitStore();
const learningPathStore = useLearningPathStore();
const { t } = useTranslation();
const submittables = computed(() => {
const learningPath = learningPathStore.learningPathForUser(
props.courseSession.course.slug,
userStore.id
);
if (!learningPath) {
return [];
}
return learningPath.circles
.filter((circle) => props.selectedCircles.includes(circle.title))
.flatMap((circle) => {
const learningContents = circle.flatLearningContents.filter(
(lc) =>
lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedback"
);
return learningContents.map((lc) => {
return {
id: lc.id,
circleName: circle.title,
frontendUrl: lc.frontend_url,
title: getLearningContentType(lc),
showDetailsText: getShowDetailsText(lc),
detailsLink: getDetailsLink(lc),
content: lc,
};
});
}) as Submittable[];
});
const isFeedback = (lc: LearningContent) => {
return lc.content_type === "learnpath.LearningContentFeedback";
};
const isAssignment = (lc: LearningContent) => {
return lc.content_type === "learnpath.LearningContentAssignment";
};
const getLearningContentType = (lc: LearningContent) => {
if (isAssignment(lc)) {
const lcTypeData = learningContentTypeData(lc);
if ((lc as LearningContentAssignment).assignment_type === "REFLECTION") {
return lcTypeData.title;
}
return `${lcTypeData.title}: ${lc.title}`;
}
return t("Feedback: Feedback zum Lehrgang");
};
const getShowDetailsText = (lc: LearningContent) => {
if (isAssignment(lc)) {
const assignmentType = (lc as LearningContentAssignment).assignment_type;
if (assignmentType === "CASEWORK" || assignmentType === "REFLECTION") {
return t("Ergebnisse anschauen");
} else if (assignmentType === "PREP_ASSIGNMENT") {
return t("Status anschauen");
}
}
return t("Feedback anschauen");
};
const getDetailsLink = (lc: LearningContent) => {
if (isFeedback(lc)) {
return `cockpit/feedback/${lc.parentCircle.id}`;
}
return `cockpit/assignment/${lc.id}`;
};
const getIconName = (lc: LearningContent) => {
if (isAssignment(lc)) {
const assignmentType = (lc as LearningContentAssignment).assignment_type;
if (assignmentType === "PREP_ASSIGNMENT" || assignmentType === "CASEWORK") {
return "it-icon-assignment-large";
} else if (assignmentType === "REFLECTION") {
return "it-icon-test-large";
}
}
return "it-icon-feedback-large";
};
</script>
<template>
<div class="bg-white px-6 py-2">
<div v-if="cockpitStore.courseSessionUsers" class="divide-y divide-gray-500">
<div
v-for="submittable in submittables"
:key="submittable.id"
class="flex flex-row justify-between py-4"
>
<div class="flex w-1/3 flex-row items-center space-x-4 pr-2">
<component :is="getIconName(submittable.content)" class="h-9 w-9"></component>
<div class="flex flex-col">
<h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3>
<p class="text-gray-800">Circle «{{ submittable.circleName }}»</p>
</div>
</div>
<AssignmentSubmissionProgress
v-if="isAssignment(submittable.content)"
:course-session="props.courseSession"
:learning-content-assignment="submittable.content as LearningContentAssignment"
:show-title="false"
class="grow pr-8"
/>
<FeedbackSubmissionProgress
v-if="isFeedback(submittable.content)"
:course-session="props.courseSession"
:circle-id="submittable.content.parentCircle.id"
class="grow pr-8"
></FeedbackSubmissionProgress>
<div class="flex w-1/4 items-center justify-end">
<button class="btn-primary">
<router-link
:to="submittable.detailsLink"
:data-cy="`show-details-btn-${submittable.content.slug}`"
>
{{ submittable.showDetailsText }}
</router-link>
</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -23,6 +23,7 @@ import dayjs from "dayjs";
import * as log from "loglevel";
import { computed, onMounted, reactive } from "vue";
import { useTranslation } from "i18next-vue";
import { learningContentTypeData } from "@/utils/typeMaps";
const { t } = useTranslation();
const courseSession = useCurrentCourseSession();
@ -175,12 +176,7 @@ const assignmentType = computed(() => {
const subTitle = computed(() => {
if (assignment.value) {
let prefix = "Geleitete Fallarbeit";
if (assignmentType.value === "PREP_ASSIGNMENT") {
prefix = "Vorbereitungsauftrag";
} else if (assignmentType.value === "REFLECTION") {
prefix = "Reflexion";
}
const prefix = learningContentTypeData(props.learningContent).title;
return `${prefix}: ${assignment.value?.title ?? ""}`;
}
return "";

View File

@ -142,7 +142,7 @@ const router = createRouter({
props: true,
},
{
path: "assignment",
path: "assignment/:assignmentId",
component: () =>
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
props: true,
@ -155,6 +155,12 @@ const router = createRouter({
),
props: true,
},
{
path: "attendanceCheck",
component: () =>
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
props: true,
},
],
},
{

View File

@ -1,4 +1,3 @@
import type { StatusCountKey } from "@/components/ui/ItProgress.vue";
import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit";
@ -8,7 +7,6 @@ import { useUserStore } from "@/stores/user";
import type {
Assignment,
AssignmentCompletion,
AssignmentCompletionStatus,
CourseSessionUser,
LearningContentAssignment,
UserAssignmentCompletionStatus,
@ -16,15 +14,18 @@ import type {
import { sum } from "d3";
import pick from "lodash/pick";
export interface GradedUser {
user: CourseSessionUser;
grade: number;
}
export function calcLearningContentAssignments(learningPath?: LearningPath) {
// TODO: filter by circle
if (!learningPath) return [];
return learningPath.circles.flatMap((circle) => {
return circle.flatLearningContents.filter(
(lc) =>
lc.content_type === "learnpath.LearningContentAssignment" &&
lc.assignment_type === "CASEWORK"
(lc) => lc.content_type === "learnpath.LearningContentAssignment"
) as LearningContentAssignment[];
});
}
@ -41,40 +42,31 @@ export async function loadAssignmentCompletionStatusData(
const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId);
return calcUserAssignmentCompletionStatus(
courseSessionUsers,
assignmentCompletionData
);
}
export function calcUserAssignmentCompletionStatus(
courseSessionUsers: CourseSessionUser[],
assignmentCompletionStatusData: UserAssignmentCompletionStatus[]
) {
return courseSessionUsers.map((u) => {
let userStatus = "unknown" as AssignmentCompletionStatus;
const userAssignmentStatus = assignmentCompletionStatusData?.find(
(s) => s.assignment_user_id === u.user_id
const gradedUsers: GradedUser[] = [];
const assignmentSubmittedUsers: CourseSessionUser[] = [];
for (const csu of courseSessionUsers) {
const userAssignmentStatus = assignmentCompletionData.find(
(s) => s.assignment_user_id === csu.user_id
);
if (userAssignmentStatus) {
userStatus = userAssignmentStatus.completion_status;
}
let progressStatus: StatusCountKey = "UNKNOWN";
if (
["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes(
userStatus
)
userAssignmentStatus?.completion_status === "SUBMITTED" ||
userAssignmentStatus?.completion_status === "EVALUATION_IN_PROGRESS" ||
userAssignmentStatus?.completion_status === "EVALUATION_SUBMITTED"
) {
progressStatus = "SUCCESS";
assignmentSubmittedUsers.push(csu);
}
return {
userId: u.user_id,
userStatus,
progressStatus,
grade: userAssignmentStatus?.evaluation_grade ?? null,
};
});
if (userAssignmentStatus?.completion_status === "EVALUATION_SUBMITTED") {
gradedUsers.push({
user: csu,
grade: userAssignmentStatus.evaluation_grade ?? 0,
});
}
}
return {
assignmentSubmittedUsers: assignmentSubmittedUsers,
gradedUsers: gradedUsers,
total: courseSessionUsers.length,
};
}
export function findAssignmentDetail(assignmentId: number) {

View File

@ -39,8 +39,12 @@ export const useCockpitStore = defineStore({
this.cockpitSessionUser = currentUser as ExpertSessionUser;
}
if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
if (this.selectedCircles.length === 0) {
// workaround to select first circle by default, when nothing is selected...
// TODO: is this the right place to do this?
if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
}
}
if (!this.courseSessionUsers) {

View File

@ -8,7 +8,10 @@ describe("assignmentTrainer.cy.js", () => {
});
it("can open cockpit assignment page and open user assignment", () => {
cy.visit("/course/test-lehrgang/cockpit/assignment");
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben");
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
@ -18,7 +21,10 @@ describe("assignmentTrainer.cy.js", () => {
});
it("can start evaluation and store evaluation results", () => {
cy.visit("/course/test-lehrgang/cockpit/assignment");
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();

Binary file not shown.

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2023-07-19 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assignment", "0003_initial"),
]
operations = [
migrations.AlterField(
model_name="assignmentcompletion",
name="additional_json_data",
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -291,7 +291,7 @@ class AssignmentCompletion(models.Model):
)
completion_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [

View File

@ -2,7 +2,7 @@ import json
from datetime import datetime
import wagtail_factories
from dateutil.relativedelta import relativedelta, TH, TU
from dateutil.relativedelta import MO, relativedelta, TH, TU
from django.conf import settings
from django.utils import timezone
from slugify import slugify
@ -140,6 +140,20 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
)
csa.evaluation_deadline.save()
csa = CourseSessionAssignment.objects.create(
course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
),
)
next_monday = datetime.now() + relativedelta(weekday=MO(2))
csa.submission_deadline.start = timezone.make_aware(
(next_monday + relativedelta(weeks=1)).replace(
hour=23, minute=59, second=59, microsecond=0
)
)
csa.submission_deadline.save()
cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
title="Test Zürich 2022 a",

View File

@ -3,7 +3,7 @@ import random
from datetime import datetime, timedelta
import djclick as click
from dateutil.relativedelta import relativedelta, TH, TU
from dateutil.relativedelta import MO, relativedelta, TH, TU
from django.utils import timezone
from vbv_lernwelt.assignment.creators.create_assignments import (
@ -285,6 +285,20 @@ def create_course_uk_de():
)
csa.evaluation_deadline.save()
csa = CourseSessionAssignment.objects.create(
course_session=cs,
learning_content=LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
),
)
next_monday = datetime.now() + relativedelta(weekday=MO(2))
csa.submission_deadline.start = timezone.make_aware(
(next_monday + relativedelta(weeks=1)).replace(
hour=23, minute=59, second=59, microsecond=0
)
)
csa.submission_deadline.save()
# figma demo users and data
csu = CourseSessionUser.objects.create(
course_session=cs,

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2023-07-19 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0002_initial"),
]
operations = [
migrations.AlterField(
model_name="coursecompletion",
name="additional_json_data",
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -182,7 +182,7 @@ class CourseCompletion(models.Model):
choices=[(status, status.value) for status in CourseCompletionStatus],
default=CourseCompletionStatus.UNKNOWN.value,
)
additional_json_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [