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:
parent
65d527d894
commit
b970597a81
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 "";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue