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

View File

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

View File

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

View File

@ -14,7 +14,9 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
*/ */
const documents = { 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 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 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 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, "\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. * 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"]; 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. * 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"]; 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. * 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 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<{ export type UpsertAssignmentCompletionMutationVariables = Exact<{
assignmentId: Scalars['ID']['input']; assignmentId: Scalars['ID']['input'];
courseSessionId: 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 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<{ export type AssignmentCompletionQueryQueryVariables = Exact<{
assignmentId: Scalars['ID']['input']; assignmentId: Scalars['ID']['input'];
courseSessionId: 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 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 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 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>; 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import schema from "../gql/minifiedSchema.json";
import { devtoolsExchange } from "@urql/devtools"; import { devtoolsExchange } from "@urql/devtools";
import { cacheExchange } from "@urql/exchange-graphcache"; import { cacheExchange } from "@urql/exchange-graphcache";
import { Client, fetchExchange } from "@urql/vue"; import { Client, fetchExchange } from "@urql/vue";
import schema from "../gql/minifiedSchema.json";
import { import {
AssignmentCompletionMutation, AssignmentCompletionMutation,
AssignmentCompletionObjectType, AssignmentCompletionObjectType,
@ -16,6 +15,9 @@ export const graphqlClient = new Client({
devtoolsExchange, devtoolsExchange,
cacheExchange({ cacheExchange({
schema: schema, schema: schema,
keys: {
AttendanceUserType: (data) => data?.user_id?.toString() ?? null,
},
updates: { updates: {
Mutation: { Mutation: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,5 +1,28 @@
import { graphql } from "@/gql"; 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(` export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
mutation UpsertAssignmentCompletion( mutation UpsertAssignmentCompletion(
$assignmentId: ID! $assignmentId: ID!

View File

@ -1,5 +1,17 @@
import { graphql } from "@/gql"; 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(` export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
query assignmentCompletionQuery( query assignmentCompletionQuery(
$assignmentId: ID! $assignmentId: ID!

View File

@ -36,7 +36,7 @@ export function i18nextInit() {
"7518c269-cbf7-4d25-bc5c-6ceba2a8b74b", "7518c269-cbf7-4d25-bc5c-6ceba2a8b74b",
apiKey: import.meta.env.DEV ? import.meta.env.VITE_LOCIZE_API_KEY : undefined, apiKey: import.meta.env.DEV ? import.meta.env.VITE_LOCIZE_API_KEY : undefined,
fallbackLng: "de", 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", "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", "MS Teams öffnen": "MS Teams öffnen",
"Nächste Termine": "Nächste Termine",
"Passwort": "Passwort", "Passwort": "Passwort",
"Status anschauen": "Status anschauen",
"TODO: Nächste Termine": "TODO: Nächste Termine",
"Trainerunterlagen": "Trainerunterlagen", "Trainerunterlagen": "Trainerunterlagen",
"Vorbereitungsauftrag": "Vorbereitungsauftrag",
"Wissens - und Verständnisfragen": "Wissens - und Verständnisfragen",
"Zur Zeit sind keine Termine vorhanden": "Zur Zeit sind keine Termine vorhanden", "Zur Zeit sind keine Termine vorhanden": "Zur Zeit sind keine Termine vorhanden",
"assignment": { "assignment": {
"acceptConditionsDisclaimer": "Bedingungen akzeptieren und Ergebnisse abgeben", "acceptConditionsDisclaimer": "Bedingungen akzeptieren und Ergebnisse abgeben",
@ -243,5 +256,9 @@
}, },
"settings": { "settings": {
"emailNotifications": "Email Benachrichtigungen" "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" :ratio="0.2"
/> />
<OpenFeedback <OpenFeedback
v-else-if="openKeys.includes(question.key)" v-else-if="
openKeys.includes(question.key) && feedbackData.questions[question.key]
"
class="mb-8 bg-white" class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`" :title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question" :text="question.question"
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')" :answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
></OpenFeedback> ></OpenFeedback>
<HorizontalBarChart <HorizontalBarChart
v-else-if="horizontalChartKeys.includes(question.key)" v-else-if="
horizontalChartKeys.includes(question.key) &&
feedbackData.questions[question.key]
"
class="mb-8 bg-white" class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i}`" :title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question" :text="question.question"

View File

@ -1,20 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue"; import type { StatusCount } from "@/components/ui/ItProgress.vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/assignmentsPage/AssignmentSubmissionProgress.vue"; import type { GradedUser } from "@/services/assignmentService";
import { import {
findAssignmentDetail, findAssignmentDetail,
loadAssignmentCompletionStatusData, loadAssignmentCompletionStatusData,
} from "@/services/assignmentService"; } from "@/services/assignmentService";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import type { import type {
AssignmentCompletionStatus,
CourseSession, CourseSession,
CourseSessionUser,
LearningContentAssignment, LearningContentAssignment,
} from "@/types"; } from "@/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
const props = defineProps<{ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
@ -29,59 +30,53 @@ log.debug(
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const state = reactive({ const state = reactive({
statusByUser: [] as {
userStatus: AssignmentCompletionStatus;
progressStatus: StatusCountKey;
userId: string;
grade: number | null;
}[],
progressStatusCount: {} as StatusCount, progressStatusCount: {} as StatusCount,
gradedUsers: [] as GradedUser[],
assignmentSubmittedUsers: [] as CourseSessionUser[],
}); });
onMounted(async () => { onMounted(async () => {
state.statusByUser = await loadAssignmentCompletionStatusData( const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id, props.learningContentAssignment.content_assignment_id,
props.courseSession.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(() => const assignmentDetail = computed(() =>
findAssignmentDetail(props.learningContentAssignment.content_assignment_id) findAssignmentDetail(props.learningContentAssignment.content_assignment_id)
); );
</script> </script>
<template> <template>
<div v-if="state.statusByUser.length"> <div>
<div class="text-large font-bold"> <h2 class="heading-2 font-bold">
{{ props.learningContentAssignment.title }} {{ learningContentAssignment.title }}
</h2>
<div class="pt-1 underline">
Circle «{{ learningContentAssignment.parentCircle.title }}»
</div> </div>
<div v-if="assignmentDetail"> <div v-if="assignmentDetail">
<span> <span>
Abgabetermin: {{ $t("Abgabetermin Ergebnisse:") }}
{{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }} {{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }}
</span> </span>
- <template v-if="assignmentDetail.evaluation_deadline_start">
<span> <br />
Freigabetermin: <span v-if="assignmentDetail.evaluation_deadline_start">
{{ $t("Freigabetermin Bewertungen:") }}
{{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }} {{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
</span> </span>
</template>
</div> </div>
<div v-else>
<div> {{ $t("Keine Auftragsdetails verfügbar.") }}
<a
:href="props.learningContentAssignment.frontend_url"
class="link"
target="_blank"
>
Im Circle anzeigen
</a>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<!-- how to determine assignment-type? how to get AssignmentLearningContent? -->
<AssignmentSubmissionProgress <AssignmentSubmissionProgress
:course-session="courseSession" :course-session="courseSession"
:learning-content-assignment="learningContentAssignment" :learning-content-assignment="learningContentAssignment"
@ -102,9 +97,7 @@ const assignmentDetail = computed(() =>
<section class="flex w-full justify-between px-8"> <section class="flex w-full justify-between px-8">
<div <div
v-if=" v-if="
['EVALUATION_SUBMITTED'].includes( state.gradedUsers.map((gradedUser) => gradedUser.user).includes(csu)
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
)
" "
class="flex items-center" class="flex items-center"
> >
@ -116,11 +109,7 @@ const assignmentDetail = computed(() =>
<div class="ml-2">Bewertung freigegeben</div> <div class="ml-2">Bewertung freigegeben</div>
</div> </div>
<div <div
v-else-if=" v-else-if="state.assignmentSubmittedUsers.includes(csu)"
['EVALUATION_IN_PROGRESS', 'SUBMITTED'].includes(
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
)
"
class="flex items-center" class="flex items-center"
> >
<div <div
@ -130,21 +119,27 @@ const assignmentDetail = computed(() =>
</div> </div>
<div class="ml-2">Ergebnisse abgegeben</div> <div class="ml-2">Ergebnisse abgegeben</div>
</div> </div>
<div v-else></div>
<div v-if="submissionStatusForUser(csu.user_id)?.grade"> <div
Note: {{ submissionStatusForUser(csu.user_id)?.grade }} v-if="
state.gradedUsers.map((gradedUser) => gradedUser.user).includes(csu)
"
>
Note:
{{
state.gradedUsers.find((u) => u.user.user_id === csu.user_id)?.grade
}}
</div> </div>
</section> </section>
</template> </template>
<template #link> <template #link>
<router-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}`" :to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment_id}/${csu.user_id}`"
class="w-full text-right underline" class="w-full text-right underline"
data-cy="show-results" data-cy="show-results"
> >
Ergebnisse anzeigen {{ $t("Ergebnisse anzeigen") }}
</router-link> </router-link>
</template> </template>
</ItPersonRow> </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"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue"; 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 * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useUserStore } from "@/stores/user";
import { useLearningPathStore } from "@/stores/learningPath";
import { calcLearningContentAssignments } from "@/services/assignmentService";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
assignmentId: string;
}>(); }>();
log.debug("AssignmentsPage created", props.courseSlug); log.debug("AssignmentsPage created", props.courseSlug);
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const userStore = useUserStore();
const learningPathStore = useLearningPathStore();
onMounted(async () => { onMounted(async () => {
log.debug("AssignmentsPage mounted"); log.debug("AssignmentsPage mounted");
}); });
const learningContentAssignments = computed(() => { const learningContentAssignment = computed(() => {
return calcLearningContentAssignments( return calcLearningContentAssignments(
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id) learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
); ).filter((lc) => lc.id.toString() === props.assignmentId)[0];
}); });
</script> </script>
@ -40,21 +41,14 @@ const learningContentAssignments = computed(() => {
<span>{{ $t("general.back") }}</span> <span>{{ $t("general.back") }}</span>
</router-link> </router-link>
</nav> </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> <main>
<div v-for="lca in learningContentAssignments" :key="lca.id">
<div class="bg-white p-6"> <div class="bg-white p-6">
<AssignmentDetails <AssignmentDetails
v-if="learningContentAssignment"
:course-session="courseSession" :course-session="courseSession"
:learning-content-assignment="lca" :learning-content-assignment="learningContentAssignment"
/> />
</div> </div>
</div>
</main> </main>
</div> </div>
</div> </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"> <script setup lang="ts">
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue"; import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useCurrentCourseSession } from "@/composables"; 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 { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
@ -13,6 +12,7 @@ import { useUserStore } from "@/stores/user";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -60,7 +60,7 @@ const circles = computed(() => {
const selectedCirclesTitles = computed(() => { const selectedCirclesTitles = computed(() => {
return circles.value return circles.value
.filter((c) => cockpitStore.selectedCircles.includes(c.translation_key)) .filter((c) => cockpitStore.selectedCircles.includes(c.translation_key))
.map((c) => c.title); .map((c) => c.title) as string[];
}); });
function setActiveClasses(translationKey: string) { function setActiveClasses(translationKey: string) {
@ -73,8 +73,10 @@ function setActiveClasses(translationKey: string) {
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<div class="container-large"> <div class="container-large">
<div class="mb-9 flex items-center lg:flex-row"> <div class="mb-9 flex items-end justify-between">
<h1 class="heading-3">{{ $t("general.circles") }}:</h1> <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"> <ul class="ml-4 flex flex-row text-base font-bold leading-7">
<li <li
v-for="circle in circles" v-for="circle in circles"
@ -91,8 +93,9 @@ function setActiveClasses(translationKey: string) {
</li> </li>
</ul> </ul>
</div> </div>
</div>
<!-- Status --> <!-- 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 class="flex flex-col justify-between bg-white px-6 py-5">
<div> <div>
<h3 class="heading-3 mb-4 flex items-center gap-2"> <h3 class="heading-3 mb-4 flex items-center gap-2">
@ -112,21 +115,39 @@ function setActiveClasses(translationKey: string) {
</a> </a>
</div> </div>
</div> </div>
<AssignmentsTile :course-session="courseSession" /> <div class="flex flex-col justify-between bg-white p-6">
</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> <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>
<SubmissionsOverview
:course-session="courseSession"
:selected-circles="selectedCirclesTitles"
></SubmissionsOverview>
<div class="pt-4">
<!-- progress --> <!-- 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> <h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul> <ul>
<ItPersonRow <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 * as log from "loglevel";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { learningContentTypeData } from "@/utils/typeMaps";
const { t } = useTranslation(); const { t } = useTranslation();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
@ -175,12 +176,7 @@ const assignmentType = computed(() => {
const subTitle = computed(() => { const subTitle = computed(() => {
if (assignment.value) { if (assignment.value) {
let prefix = "Geleitete Fallarbeit"; const prefix = learningContentTypeData(props.learningContent).title;
if (assignmentType.value === "PREP_ASSIGNMENT") {
prefix = "Vorbereitungsauftrag";
} else if (assignmentType.value === "REFLECTION") {
prefix = "Reflexion";
}
return `${prefix}: ${assignment.value?.title ?? ""}`; return `${prefix}: ${assignment.value?.title ?? ""}`;
} }
return ""; return "";

View File

@ -142,7 +142,7 @@ const router = createRouter({
props: true, props: true,
}, },
{ {
path: "assignment", path: "assignment/:assignmentId",
component: () => component: () =>
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"), import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
props: true, props: true,
@ -155,6 +155,12 @@ const router = createRouter({
), ),
props: true, 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 { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
@ -8,7 +7,6 @@ import { useUserStore } from "@/stores/user";
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
AssignmentCompletionStatus,
CourseSessionUser, CourseSessionUser,
LearningContentAssignment, LearningContentAssignment,
UserAssignmentCompletionStatus, UserAssignmentCompletionStatus,
@ -16,15 +14,18 @@ import type {
import { sum } from "d3"; import { sum } from "d3";
import pick from "lodash/pick"; import pick from "lodash/pick";
export interface GradedUser {
user: CourseSessionUser;
grade: number;
}
export function calcLearningContentAssignments(learningPath?: LearningPath) { export function calcLearningContentAssignments(learningPath?: LearningPath) {
// TODO: filter by circle // TODO: filter by circle
if (!learningPath) return []; if (!learningPath) return [];
return learningPath.circles.flatMap((circle) => { return learningPath.circles.flatMap((circle) => {
return circle.flatLearningContents.filter( return circle.flatLearningContents.filter(
(lc) => (lc) => lc.content_type === "learnpath.LearningContentAssignment"
lc.content_type === "learnpath.LearningContentAssignment" &&
lc.assignment_type === "CASEWORK"
) as LearningContentAssignment[]; ) as LearningContentAssignment[];
}); });
} }
@ -41,41 +42,32 @@ export async function loadAssignmentCompletionStatusData(
const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId); const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId);
return calcUserAssignmentCompletionStatus( const gradedUsers: GradedUser[] = [];
courseSessionUsers, const assignmentSubmittedUsers: CourseSessionUser[] = [];
assignmentCompletionData for (const csu of courseSessionUsers) {
const userAssignmentStatus = assignmentCompletionData.find(
(s) => s.assignment_user_id === csu.user_id
); );
}
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
);
if (userAssignmentStatus) {
userStatus = userAssignmentStatus.completion_status;
}
let progressStatus: StatusCountKey = "UNKNOWN";
if ( if (
["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes( userAssignmentStatus?.completion_status === "SUBMITTED" ||
userStatus userAssignmentStatus?.completion_status === "EVALUATION_IN_PROGRESS" ||
) userAssignmentStatus?.completion_status === "EVALUATION_SUBMITTED"
) { ) {
progressStatus = "SUCCESS"; assignmentSubmittedUsers.push(csu);
} }
if (userAssignmentStatus?.completion_status === "EVALUATION_SUBMITTED") {
return { gradedUsers.push({
userId: u.user_id, user: csu,
userStatus, grade: userAssignmentStatus.evaluation_grade ?? 0,
progressStatus,
grade: userAssignmentStatus?.evaluation_grade ?? null,
};
}); });
} }
}
return {
assignmentSubmittedUsers: assignmentSubmittedUsers,
gradedUsers: gradedUsers,
total: courseSessionUsers.length,
};
}
export function findAssignmentDetail(assignmentId: number) { export function findAssignmentDetail(assignmentId: number) {
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();

View File

@ -39,9 +39,13 @@ export const useCockpitStore = defineStore({
this.cockpitSessionUser = currentUser as ExpertSessionUser; this.cockpitSessionUser = currentUser as ExpertSessionUser;
} }
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) { if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key]; this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
} }
}
if (!this.courseSessionUsers) { if (!this.courseSessionUsers) {
throw `No courseSessionUsers data found for user`; throw `No courseSessionUsers data found for user`;

View File

@ -8,7 +8,10 @@ describe("assignmentTrainer.cy.js", () => {
}); });
it("can open cockpit assignment page and open user assignment", () => { 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"]').should("contain", "Ergebnisse abgegeben");
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click(); 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", () => { 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(); 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) completion_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict, blank=True)
class Meta: class Meta:
constraints = [ constraints = [

View File

@ -2,7 +2,7 @@ import json
from datetime import datetime from datetime import datetime
import wagtail_factories 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.conf import settings
from django.utils import timezone from django.utils import timezone
from slugify import slugify 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.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( cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Zürich 2022 a", title="Test Zürich 2022 a",

View File

@ -3,7 +3,7 @@ import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
import djclick as click 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 django.utils import timezone
from vbv_lernwelt.assignment.creators.create_assignments import ( from vbv_lernwelt.assignment.creators.create_assignments import (
@ -285,6 +285,20 @@ def create_course_uk_de():
) )
csa.evaluation_deadline.save() 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 # figma demo users and data
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, 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], choices=[(status, status.value) for status in CourseCompletionStatus],
default=CourseCompletionStatus.UNKNOWN.value, default=CourseCompletionStatus.UNKNOWN.value,
) )
additional_json_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict, blank=True)
class Meta: class Meta:
constraints = [ constraints = [