diff --git a/README.md b/README.md index 68c667d1..5ba494da 100644 --- a/README.md +++ b/README.md @@ -309,3 +309,15 @@ graphql schema. - The `id` field has to be a string? - What about the generated types from `codegen`? Hand written types seem to be better. - The functions in `cacheExchange` should be nearer the concrete implementation... + +## Load prod data for testing + +1. Checkout the [vbv-devops](https://bitbucket.org/iterativ/iterativ-devops/src/master/) repository +2. Change into the `backups` directory +3. Run `python3 check_vbv_backup.py`. This downloads the latest backup from S3 and restores it to the `vbv-lernwelt` database. +4. Reset all user passwords. Open `shell_plus` in the `server` directory of the `vbv_lernwelt` repository and run + + ```python + for csu in CourseSessionUser.objects.all(): + csu.user.set_password("test") + ``` diff --git a/client/src/components/dashboard/CoursePanel.vue b/client/src/components/dashboard/CoursePanel.vue index b73210f3..89a3a335 100644 --- a/client/src/components/dashboard/CoursePanel.vue +++ b/client/src/components/dashboard/CoursePanel.vue @@ -10,6 +10,7 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v import type { DashboardCourseConfigType, WidgetType } from "@/services/dashboard"; import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils"; import { computed } from "vue"; +import TrainingResponsibleStatistics from "./TrainingResponsibleStatistics.vue"; const mentorWidgets = [ "MentorTasksWidget", @@ -71,6 +72,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string } cyKey: "progress-dashboard-continue-course-link", }; } + if (props.courseConfig?.role_key === "Ausbildungsverantwortlicher") { + return { + href: getLearningPathUrl(props.courseConfig?.course_slug), + text: "a.Vorschau Teilnehmer", + cyKey: "tr-dashboard-link", + }; + } return { href: getLearningPathUrl(props.courseConfig?.course_slug), text: "Weiter lernen", @@ -193,6 +201,12 @@ function hasActionButton(): boolean { :agent-role="courseConfig.role_key" /> + + diff --git a/client/src/components/dashboard/TrainingResponsibleStatistics.vue b/client/src/components/dashboard/TrainingResponsibleStatistics.vue new file mode 100644 index 00000000..d6a36054 --- /dev/null +++ b/client/src/components/dashboard/TrainingResponsibleStatistics.vue @@ -0,0 +1,72 @@ + + + + + + {{ $t("Kosten in") }} {{ new Date().getFullYear() }} + + + + + {{ formatCurrencyChfCentimes(totalCostInCurrentYear) }} + + CHF + + + + + + {{ $t("Teilnehmer im 2024") }} + + + + {{ attendanceCountInCurrentYear }} + + + {{ $t("Teilnehmer") }} + + + + + + diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index 065feb95..b9c76014 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -28,6 +28,7 @@ const documents = { "\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n": types.DashboardCourseDataDocument, "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_title\n competence_certificate_id\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_evaluation_percent\n average_passed\n competence_certificate_weight\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument, "\n query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\n }\n }\n }\n }\n }\n": types.MentorCourseStatisticsDocument, + "\n query trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n": types.TrainingResponsibleStatisticsDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, }; @@ -105,6 +106,10 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\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 mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\n }\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 query trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n"): (typeof documents)["\n query trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 5f3d2297..0783825d 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -359,6 +359,13 @@ export type CoreUserLanguageChoices = /** Italiano */ | 'IT'; +export type CostForYear = { + __typename?: 'CostForYear'; + _id: Scalars['ID']['output']; + total_cost: Scalars['Int']['output']; + year: Scalars['Int']['output']; +}; + export type CourseConfigurationObjectType = { __typename?: 'CourseConfigurationObjectType'; enable_circle_documents: Scalars['Boolean']['output']; @@ -922,6 +929,13 @@ export type MutationUpsertAssignmentCompletionArgs = { learning_content_page_id?: InputMaybe; }; +export type ParticipantsForYear = { + __typename?: 'ParticipantsForYear'; + _id: Scalars['ID']['output']; + participants: Array>; + year: Scalars['Int']['output']; +}; + export type PerformanceCriteriaObjectType = CoursePageInterface & { __typename?: 'PerformanceCriteriaObjectType'; competence_id: Scalars['String']['output']; @@ -991,6 +1005,7 @@ export type Query = { learning_content_video?: Maybe; learning_path?: Maybe; mentor_course_statistics?: Maybe; + training_responsible_statistics?: Maybe; }; @@ -1063,6 +1078,11 @@ export type QueryMentorCourseStatisticsArgs = { course_id: Scalars['ID']['input']; }; + +export type QueryTrainingResponsibleStatisticsArgs = { + course_session_id: Scalars['ID']['input']; +}; + export type SendFeedbackMutation = { __typename?: 'SendFeedbackMutation'; /** May contain more than one error for same field. */ @@ -1113,6 +1133,14 @@ export type TopicObjectType = CoursePageInterface & { translation_key: Scalars['String']['output']; }; +export type TrainingResponsibleStatisticsType = { + __typename?: 'TrainingResponsibleStatisticsType'; + _id: Scalars['ID']['output']; + cost_per_year: Array>; + course_session_id: Scalars['ID']['output']; + participants_per_year: Array>; +}; + export type UpdateCourseProfileError = { __typename?: 'UpdateCourseProfileError'; message?: Maybe; @@ -1456,6 +1484,13 @@ export type MentorCourseStatisticsQueryVariables = Exact<{ export type MentorCourseStatisticsQuery = { __typename?: 'Query', mentor_course_statistics?: { __typename?: 'BaseStatisticsType', _id: string, course_id: string, course_title: string, course_slug: string, course_session_selection_ids: Array, user_selection_ids?: Array | null, course_session_properties: { __typename?: 'StatisticsCourseSessionPropertiesType', _id: string, generations: Array, sessions: Array<{ __typename?: 'StatisticsCourseSessionDataType', id: string, name: string, region: string }>, circles: Array<{ __typename?: 'StatisticsCircleDataType', id: string, name: string }> }, assignments: { __typename?: 'AssignmentsStatisticsType', _id: string, summary: { __typename?: 'AssignmentStatisticsSummaryType', _id: string, completed_count: number, average_passed: number, total_passed: number, total_failed: number, average_evaluation_percent?: number | null }, records: Array<{ __typename?: 'AssignmentStatisticsRecordType', _id: string, course_session_id: string, course_session_assignment_id: string, course_session_title?: string | null, circle_id: string, generation: string, region: string, assignment_title: string, assignment_type_translation_key: string, competence_certificate_id?: string | null, competence_certificate_title?: string | null, details_url: string, learning_content_id: string, deadline: string, metrics: { __typename?: 'AssignmentCompletionMetricsType', _id: string, passed_count: number, failed_count: number, unranked_count: number, ranking_completed: boolean, competence_certificate_weight?: number | null, average_evaluation_percent?: number | null, average_passed: number } }> } } | null }; +export type TrainingResponsibleStatisticsQueryVariables = Exact<{ + courseSessionId: Scalars['ID']['input']; +}>; + + +export type TrainingResponsibleStatisticsQuery = { __typename?: 'Query', training_responsible_statistics?: { __typename?: 'TrainingResponsibleStatisticsType', _id: string, cost_per_year: Array<{ __typename?: 'CostForYear', _id: string, year: number, total_cost: number } | null>, participants_per_year: Array<{ __typename?: 'ParticipantsForYear', _id: string, year: number, participants: Array<{ __typename?: 'CourseSessionUserType', id: string, chosen_profile: string } | null> } | null> } | null }; + export type SendFeedbackMutationMutationVariables = Exact<{ courseSessionId: Scalars['ID']['input']; learningContentId: Scalars['ID']['input']; @@ -1482,4 +1517,5 @@ export const DashboardProgressDocument = {"kind":"Document","definitions":[{"kin export const DashboardCourseDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardCourseData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_progress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_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":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_to_continue_id"}}]}}]}}]} as unknown as DocumentNode; export const CourseStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseStatistics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_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":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_title"}},{"kind":"Field","name":{"kind":"Name","value":"course_slug"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"sessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}}]}},{"kind":"Field","name":{"kind":"Name","value":"generations"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_count"}},{"kind":"Field","name":{"kind":"Name","value":"participant_count"}},{"kind":"Field","name":{"kind":"Name","value":"expert_count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_day_presences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}},{"kind":"Field","name":{"kind":"Name","value":"participants_total"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"days_completed"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"feedback_responses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"experts"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"total_responses"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"completed_count"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_failed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_assignment_id"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type_translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_title"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_id"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"passed_count"}},{"kind":"Field","name":{"kind":"Name","value":"failed_count"}},{"kind":"Field","name":{"kind":"Name","value":"unranked_count"}},{"kind":"Field","name":{"kind":"Name","value":"ranking_completed"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_weight"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"success_total"}},{"kind":"Field","name":{"kind":"Name","value":"fail_total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"success_count"}},{"kind":"Field","name":{"kind":"Name","value":"fail_count"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const MentorCourseStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"mentorCourseStatistics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentRole"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mentor_course_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"agent_role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentRole"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_title"}},{"kind":"Field","name":{"kind":"Name","value":"course_slug"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"user_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"sessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}}]}},{"kind":"Field","name":{"kind":"Name","value":"generations"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"completed_count"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_failed"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_assignment_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_title"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type_translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_id"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_title"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}},{"kind":"Field","name":{"kind":"Name","value":"learning_content_id"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"passed_count"}},{"kind":"Field","name":{"kind":"Name","value":"failed_count"}},{"kind":"Field","name":{"kind":"Name","value":"unranked_count"}},{"kind":"Field","name":{"kind":"Name","value":"ranking_completed"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_weight"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const TrainingResponsibleStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"trainingResponsibleStatistics"},"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":"training_responsible_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_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":"cost_per_year"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"year"}},{"kind":"Field","name":{"kind":"Name","value":"total_cost"}}]}},{"kind":"Field","name":{"kind":"Name","value":"participants_per_year"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"year"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chosen_profile"}}]}}]}}]}}]}}]} as unknown as DocumentNode; 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":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"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":"learning_content_type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"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":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"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; \ No newline at end of file diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index 1c644eb4..a68ce255 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,6 +1,7 @@ type Query { course_statistics(course_id: ID!): CourseStatisticsType mentor_course_statistics(course_id: ID!, agent_role: String!): BaseStatisticsType + training_responsible_statistics(course_session_id: ID!): TrainingResponsibleStatisticsType course_progress(course_id: ID!): CourseProgressType dashboard_config: [DashboardConfigType!]! learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType @@ -200,42 +201,61 @@ type BaseStatisticsType { course_session_properties: StatisticsCourseSessionPropertiesType! } -type CourseProgressType { +type TrainingResponsibleStatisticsType { _id: ID! - course_id: ID! - session_to_continue_id: ID - competence: ProgressDashboardCompetenceType - assignment: ProgressDashboardAssignmentType + course_session_id: ID! + cost_per_year: [CostForYear]! + participants_per_year: [ParticipantsForYear]! } -type ProgressDashboardCompetenceType { +type CostForYear { _id: ID! - total_count: Int! - success_count: Int! - fail_count: Int! + year: Int! + total_cost: Int! } -type ProgressDashboardAssignmentType { +type ParticipantsForYear { _id: ID! - total_count: Int! - points_max_count: Int! - points_achieved_count: Int! + year: Int! + participants: [CourseSessionUserType]! } -type DashboardConfigType { +type CourseSessionUserType { + id: UUID! + chosen_profile: String! + course_session: CourseSessionObjectType! +} + +""" +Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects +in fields, resolvers and input. +""" +scalar UUID + +type CourseSessionObjectType { id: ID! - name: String! - slug: String! - dashboard_type: DashboardType! - course_configuration: CourseConfigurationObjectType! + created_at: DateTime! + updated_at: DateTime! + course: CourseObjectType! + title: String! + start_date: Date + end_date: Date + attendance_courses: [CourseSessionAttendanceCourseObjectType!]! + assignments: [CourseSessionAssignmentObjectType!]! + edoniq_tests: [CourseSessionEdoniqTestObjectType!]! + users: [CourseSessionUserObjectsType!]! } -enum DashboardType { - STATISTICS_DASHBOARD - PROGRESS_DASHBOARD - SIMPLE_DASHBOARD - MENTOR_DASHBOARD - PRAXISBILDNER_DASHBOARD +type CourseObjectType { + id: ID! + title: String! + category_name: String! + slug: String! + configuration: CourseConfigurationObjectType! + learning_path: LearningPathObjectType! + action_competences: [ActionCompetenceObjectType!]! + profiles: [String] + course_session_users(id: String): [CourseSessionUserType]! } type CourseConfigurationObjectType { @@ -270,20 +290,8 @@ interface CoursePageInterface { course: CourseObjectType } -type CourseObjectType { - id: ID! - title: String! - category_name: String! - slug: String! - configuration: CourseConfigurationObjectType! - learning_path: LearningPathObjectType! - action_competences: [ActionCompetenceObjectType!]! - profiles: [String] - course_session_users(id: String): [CourseSessionUserType]! -} - -type ActionCompetenceObjectType implements CoursePageInterface { - competence_id: String! +type TopicObjectType implements CoursePageInterface { + is_visible: Boolean! id: ID! title: String! slug: String! @@ -292,11 +300,13 @@ type ActionCompetenceObjectType implements CoursePageInterface { translation_key: String! frontend_url: String! course: CourseObjectType - performance_criteria: [PerformanceCriteriaObjectType!]! + circles: [CircleObjectType!]! } -type PerformanceCriteriaObjectType implements CoursePageInterface { - competence_id: String! +type CircleObjectType implements CoursePageInterface { + description: String! + goals: String! + is_base_circle: Boolean! id: ID! title: String! slug: String! @@ -305,7 +315,21 @@ type PerformanceCriteriaObjectType implements CoursePageInterface { translation_key: String! frontend_url: String! course: CourseObjectType - learning_unit: LearningUnitObjectType + learning_sequences: [LearningSequenceObjectType!]! + profiles: [String]! +} + +type LearningSequenceObjectType implements CoursePageInterface { + icon: String! + id: ID! + title: String! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + learning_units: [LearningUnitObjectType!]! } type LearningUnitObjectType implements CoursePageInterface { @@ -345,30 +369,30 @@ type CircleLightObjectType { slug: String! } -type CourseSessionUserType { - id: UUID! - chosen_profile: String! - course_session: CourseSessionObjectType! +type PerformanceCriteriaObjectType implements CoursePageInterface { + competence_id: String! + id: ID! + title: String! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + learning_unit: LearningUnitObjectType } -""" -Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects -in fields, resolvers and input. -""" -scalar UUID - -type CourseSessionObjectType { +type ActionCompetenceObjectType implements CoursePageInterface { + competence_id: String! id: ID! - created_at: DateTime! - updated_at: DateTime! - course: CourseObjectType! title: String! - start_date: Date - end_date: Date - attendance_courses: [CourseSessionAttendanceCourseObjectType!]! - assignments: [CourseSessionAssignmentObjectType!]! - edoniq_tests: [CourseSessionEdoniqTestObjectType!]! - users: [CourseSessionUserObjectsType!]! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + performance_criteria: [PerformanceCriteriaObjectType!]! } """ @@ -717,46 +741,42 @@ type CourseSessionUserExpertCircleType { slug: String! } -type TopicObjectType implements CoursePageInterface { - is_visible: Boolean! - id: ID! - title: String! - slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - circles: [CircleObjectType!]! +type CourseProgressType { + _id: ID! + course_id: ID! + session_to_continue_id: ID + competence: ProgressDashboardCompetenceType + assignment: ProgressDashboardAssignmentType } -type CircleObjectType implements CoursePageInterface { - description: String! - goals: String! - is_base_circle: Boolean! - id: ID! - title: String! - slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - learning_sequences: [LearningSequenceObjectType!]! - profiles: [String]! +type ProgressDashboardCompetenceType { + _id: ID! + total_count: Int! + success_count: Int! + fail_count: Int! } -type LearningSequenceObjectType implements CoursePageInterface { - icon: String! +type ProgressDashboardAssignmentType { + _id: ID! + total_count: Int! + points_max_count: Int! + points_achieved_count: Int! +} + +type DashboardConfigType { id: ID! - title: String! + name: String! slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - learning_units: [LearningUnitObjectType!]! + dashboard_type: DashboardType! + course_configuration: CourseConfigurationObjectType! +} + +enum DashboardType { + STATISTICS_DASHBOARD + PROGRESS_DASHBOARD + SIMPLE_DASHBOARD + MENTOR_DASHBOARD + PRAXISBILDNER_DASHBOARD } type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface { diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index c6712dfd..b601f647 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -26,6 +26,7 @@ export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType"; export const CompetencesStatisticsType = "CompetencesStatisticsType"; export const ContentDocumentObjectType = "ContentDocumentObjectType"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; +export const CostForYear = "CostForYear"; export const CourseConfigurationObjectType = "CourseConfigurationObjectType"; export const CourseObjectType = "CourseObjectType"; export const CoursePageInterface = "CoursePageInterface"; @@ -74,6 +75,7 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType"; export const LearningUnitObjectType = "LearningUnitObjectType"; export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices"; export const Mutation = "Mutation"; +export const ParticipantsForYear = "ParticipantsForYear"; export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType"; export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType"; export const ProgressDashboardAssignmentType = "ProgressDashboardAssignmentType"; @@ -86,6 +88,7 @@ export const StatisticsCourseSessionPropertiesType = "StatisticsCourseSessionPro export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSessionsSelectionMetricType"; export const String = "String"; export const TopicObjectType = "TopicObjectType"; +export const TrainingResponsibleStatisticsType = "TrainingResponsibleStatisticsType"; export const UUID = "UUID"; export const UpdateCourseProfileError = "UpdateCourseProfileError"; export const UpdateCourseProfileResult = "UpdateCourseProfileResult"; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index 8beea4f4..246e060a 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -607,3 +607,24 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(` } } `); + +export const TRAINING_RESPONSIBLE_STATISTICS = graphql(` + query trainingResponsibleStatistics($courseSessionId: ID!) { + training_responsible_statistics(course_session_id: $courseSessionId) { + _id + cost_per_year { + _id + year + total_cost + } + participants_per_year { + _id + year + participants { + id + chosen_profile + } + } + } + } +`); diff --git a/client/src/pages/dashboard/DashboardCostPage.vue b/client/src/pages/dashboard/DashboardCostPage.vue new file mode 100644 index 00000000..bfc8428d --- /dev/null +++ b/client/src/pages/dashboard/DashboardCostPage.vue @@ -0,0 +1,65 @@ + + + + + + + + + + + + {{ $t("general.back") }} + + {{ $t("a.Kosten") }} + + + + {{ entry?.year }} + + {{ participantsForYear(entry?.year ?? 0) }} {{ $t("a.Teilnehmer") }} + + + {{ formatCurrencyChfCentimes(entry?.total_cost) }} CHF + + + + + + + + diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue index 6e89792f..706f9018 100644 --- a/client/src/pages/dashboard/DashboardPersonsPage.vue +++ b/client/src/pages/dashboard/DashboardPersonsPage.vue @@ -168,6 +168,29 @@ const roles = computed(() => { }); const selectedRole = ref(roles.value[0]); +const chosenProfiles = computed(() => { + const values = _(dashboardPersons.value) + .map((cs) => { + return Object.assign({}, cs, { + name: cs.chosen_profile, + id: cs.chosen_profile, + }); + }) + .filter((cs) => cs.chosen_profile !== "all") + .uniqBy("id") + .orderBy("name") + .value(); + + return [ + { + id: "all", + name: `${t("Zulassungsprofil")}: ${t("a.Alle")}`, + }, + ...values, + ]; +}); +const selectedChosenProfile = ref(chosenProfiles.value[0]); + const filteredPersons = computed(() => { return _.orderBy( dashboardPersons.value @@ -208,6 +231,12 @@ const filteredPersons = computed(() => { return person.course_sessions.some( (cs) => cs.user_role_display === selectedRole.value.id ); + }) + .filter((person) => { + if (selectedChosenProfile.value.id === "") { + return true; + } + return person.chosen_profile === selectedChosenProfile.value.id; }), ["last_name", "first_name"] ); @@ -219,7 +248,8 @@ const filtersVisible = computed(() => { courseSessions.value.length > 2 || regions.value.length > 2 || generations.value.length > 2 || - roles.value.length > 2 + roles.value.length > 2 || + chosenProfiles.value.length > 2 ); }); @@ -342,6 +372,14 @@ watch(selectedRegion, () => { :items="roles" borderless > + + import("@/pages/dashboard/DashboardDueDatesPage.vue"), }, + { + path: "/dashboard/cost/:courseSessionId", + component: () => import("@/pages/dashboard/DashboardCostPage.vue"), + props: true, + }, { path: "/course/:courseSlug/media", props: true, diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 3c701010..5045e37e 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -4,6 +4,7 @@ import { DASHBOARD_COURSE_SESSION_PROGRESS, DASHBOARD_COURSE_STATISTICS, DASHBOARD_MENTOR_COMPETENCE_SUMMARY, + TRAINING_RESPONSIBLE_STATISTICS, } from "@/graphql/queries"; import { itGetCached, itPost } from "@/fetchHelpers"; @@ -12,6 +13,8 @@ import type { CourseProgressType, CourseStatisticsType, DashboardConfigType, + TrainingResponsibleStatisticsQuery, + TrainingResponsibleStatisticsType, } from "@/gql/graphql"; import type { DashboardPersonsPageMode, @@ -34,7 +37,8 @@ export type DashboardRoleKeyType = | "Member" | "MentorUK" | "MentorVV" - | "Berufsbildner"; + | "Berufsbildner" + | "Ausbildungsverantwortlicher"; export type WidgetType = | "ProgressWidget" @@ -44,7 +48,8 @@ export type WidgetType = | "MentorCompetenceWidget" | "CompetenceCertificateWidget" | "UKStatisticsWidget" - | "UKBerufsbildnerStatisticsWidget"; + | "UKBerufsbildnerStatisticsWidget" + | "TrainingResponsibleStatisticsWidget"; export type DashboardPersonCourseSessionType = { id: string; @@ -78,6 +83,7 @@ export type DashboardPersonType = { passed_count: number; failed_count: number; }; + chosen_profile: string; }; export type DashboardCourseConfigType = { @@ -118,6 +124,31 @@ export const fetchStatisticData = async ( } }; +export const fetchTrainingResponsibleStatistics = async ( + courseSessionId: string +): Promise => { + try { + const res = await graphqlClient.query(TRAINING_RESPONSIBLE_STATISTICS, { + courseSessionId, + }); + + if (res.error) { + console.error( + "Error fetching training responsible statistics for course session ID:", + courseSessionId, + res.error + ); + } + return res.data || null; + } catch (error) { + console.error( + `Error fetching training responsible statistics for course session ID: ${courseSessionId}`, + error + ); + return null; + } +}; + export const fetchProgressData = async ( courseId: string ): Promise => { diff --git a/client/src/utils/format_currency_chf.ts b/client/src/utils/format_currency_chf.ts new file mode 100644 index 00000000..6c5b4365 --- /dev/null +++ b/client/src/utils/format_currency_chf.ts @@ -0,0 +1,17 @@ +export function formatCurrencyChfCentimes(amount: number | undefined) { + if (!amount) { + return formatCurrencyChf(undefined); + } + return formatCurrencyChf(amount / 100); +} + +// 10378.2 => 10'378 +export function formatCurrencyChf(amount: number | undefined) { + if (!amount) { + return "?"; + } + return new Intl.NumberFormat("de-CH", { + style: "decimal", + maximumFractionDigits: 0, + }).format(amount); +} diff --git a/scripts/create_ausbildungsverantwortlicher_mobi.py b/scripts/create_ausbildungsverantwortlicher_mobi.py new file mode 100644 index 00000000..c0435458 --- /dev/null +++ b/scripts/create_ausbildungsverantwortlicher_mobi.py @@ -0,0 +1,48 @@ +import os +import sys + +import django +from django.contrib.auth.hashers import make_password + +sys.path.append("../server") + +os.environ.setdefault("IT_APP_ENVIRONMENT", "local") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base") +django.setup() + +from vbv_lernwelt.course.consts import UK_COURSE_IDS +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.learning_mentor.models import ( + AgentParticipantRelation, + AgentParticipantRoleType, +) + + +def main(): + berufsbildner, _ = User.objects.get_or_create( + id="5f984be9-3024-4169-9c7b-c9e827c18fd8" + ) + berufsbildner.username = "training-responsible-mobi@example.com" + berufsbildner.email = "training-responsible-mobi@example.com" + berufsbildner.language = "de" + berufsbildner.first_name = "Ausbildungsverantwortlicher" + berufsbildner.last_name = "Mobi" + berufsbildner.password = make_password("test") + berufsbildner.save() + + for csu in ( + CourseSessionUser.objects.filter(user__username__contains="@mobi") + .filter(course_session__course__configuration__is_uk=False) + .filter(role=CourseSessionUser.Role.MEMBER.value) + .exclude(course_session_id__in=UK_COURSE_IDS) + ): + AgentParticipantRelation.objects.get_or_create( + agent=berufsbildner, + participant=csu, + role=AgentParticipantRoleType.BERUFSBILDNER.value, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/create_berufsbildner_mobi.py b/scripts/create_berufsbildner_mobi.py index 85eaeccd..f92d193d 100644 --- a/scripts/create_berufsbildner_mobi.py +++ b/scripts/create_berufsbildner_mobi.py @@ -22,17 +22,17 @@ def main(): berufsbildner, _ = User.objects.get_or_create( id="5f984be9-3024-4169-9c7b-c9e827c18fd8" ) - berufsbildner.username = "berufsbildner-mobi@example.com" - berufsbildner.email = "berufsbildner-mobi@example.com" + berufsbildner.username = "training-responsible-mobi@example.com" + berufsbildner.email = "training-responsible-mobi@example.com" berufsbildner.language = "de" - berufsbildner.first_name = "Berufsbildner" + berufsbildner.first_name = "Ausbildungsverantwortlicher" berufsbildner.last_name = "Mobi" berufsbildner.password = make_password("test") berufsbildner.save() for csu in ( CourseSessionUser.objects.filter(user__username__contains="@mobi") - .filter(course_session__course__configuration__is_uk=True) + .filter(course_session__course__configuration__is_uk=False) .filter(role=CourseSessionUser.Role.MEMBER.value) .exclude(course_session_id__in=[4, 5, 6]) ): diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py index dd19d88a..2f0b9722 100644 --- a/server/vbv_lernwelt/core/constants.py +++ b/server/vbv_lernwelt/core/constants.py @@ -26,6 +26,7 @@ TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900" TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b" TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b" TEST_BERUFSBILDNER1_USER_ID = "bb83dde0-27e7-4859-8acb-a323025d712c" +TEST_LERNBEGLEITER1_USER_ID = "ffeedde0-27e7-ff59-8aff-a3230ffd712c" TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index be12dad0..58d547a4 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -7,6 +7,7 @@ from environs import Env from vbv_lernwelt.core.constants import ( ADMIN_USER_ID, TEST_BERUFSBILDNER1_USER_ID, + TEST_LERNBEGLEITER1_USER_ID, TEST_MENTOR1_USER_ID, TEST_STUDENT1_USER_ID, TEST_STUDENT1_VV_USER_ID, @@ -419,6 +420,15 @@ def create_default_users(default_password="test", set_avatar=False): language="de", avatar_image="uk1.patrizia.huggel.jpg", ) + _create_user( + _id=TEST_LERNBEGLEITER1_USER_ID, + email="test-lernbegleiter1@example.com", + first_name="Bruno", + last_name="Banani-Lernbegleiter", + password=default_password, + language="de", + avatar_image="uk1.patrizia.huggel.jpg", + ) _create_student_user( id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, email="test-student-and-mentor2@example.com", diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index b0a92e93..6bce8418 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,3 +1,4 @@ +from hashlib import md5 from typing import Dict, List, Set, Tuple import graphene @@ -19,6 +20,7 @@ from vbv_lernwelt.dashboard.graphql.types.dashboard import ( DashboardType, ProgressDashboardAssignmentType, ProgressDashboardCompetenceType, + TrainingResponsibleStatisticsType, ) from vbv_lernwelt.iam.permissions import ( can_view_course_session, @@ -69,6 +71,11 @@ class DashboardQuery(graphene.ObjectType): agent_role=graphene.String(required=True), ) + training_responsible_statistics = graphene.Field( + TrainingResponsibleStatisticsType, + course_session_id=graphene.ID(required=True), + ) + course_progress = graphene.Field( CourseProgressType, course_id=graphene.ID(required=True) ) @@ -124,6 +131,13 @@ class DashboardQuery(graphene.ObjectType): return _agent_course_statistics(user, course_id, role=agent_role) + @staticmethod + def resolve_training_responsible_statistics(root, info, course_session_id: str): # noqa + return TrainingResponsibleStatisticsType( + _id=course_session_id, # noqa + course_session_id=course_session_id, # noqa + ) + def resolve_dashboard_config(root, info): # noqa user = info.context.user diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 69a1917a..4f9196a6 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,7 +1,13 @@ +from datetime import datetime +from itertools import groupby import graphene from graphene import Enum -from vbv_lernwelt.course.graphql.types import CourseConfigurationObjectType +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.course.graphql.types import ( + CourseConfigurationObjectType, + CourseSessionUserType, +) from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.graphql.types.assignment import ( AssignmentsStatisticsType, @@ -20,7 +26,13 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import ( FeedbackStatisticsResponsesType, feedback_responses, ) +from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation from vbv_lernwelt.learnpath.models import Circle +from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState +from vbv_lernwelt.shop.views import ( + COURSE_SESSION_ID_TO_PRODUCT_SKU, + PRODUCT_SKU_TO_COURSE_SESSION_ID, +) class StatisticsCourseSessionDataType(graphene.ObjectType): @@ -242,3 +254,75 @@ class CourseStatisticsType(BaseStatisticsType): participant_count=participant_count, # noqa expert_count=expert_count, # noqa ) + + +class CostForYear(graphene.ObjectType): + _id = graphene.ID(required=True) + year = graphene.Int(required=True) + # In centimes CHF + total_cost = graphene.Int(required=True) + + +class ParticipantsForYear(graphene.ObjectType): + _id = graphene.ID(required=True) + year = graphene.Int(required=True) + participants = graphene.List(CourseSessionUserType, required=True) + + +class TrainingResponsibleStatisticsType(graphene.ObjectType): + _id = graphene.ID(required=True) + course_session_id = graphene.ID(required=True) + + cost_per_year = graphene.List(CostForYear, required=True) + participants_per_year = graphene.List(ParticipantsForYear, required=True) + + @staticmethod + def resolve_cost_per_year(root, info): # noqa + user = info.context.user + relations_qs = AgentParticipantRelation.objects.filter( + agent=user, + participant__course_session=root.course_session_id, + ) + users = relations_qs.values_list("participant__user", flat=True) + + sku = COURSE_SESSION_ID_TO_PRODUCT_SKU.get(int(root.course_session_id)) + checkout_information = CheckoutInformation.objects.filter( + state=CheckoutState.PAID, + product_sku=sku, + user__in=users, + ) + + grouped_checkouts = groupby( + sorted(checkout_information, key=lambda x: x.created_at.year), + key=lambda x: x.created_at.year, + ) + return [ + CostForYear( + _id=f"{root.course_session_id} {year}", + year=year, + total_cost=sum(c.product_price for c in checkouts), + ) + for year, checkouts in grouped_checkouts + ] + + @staticmethod + def resolve_participants_per_year(root, info): + user = info.context.user + course_session_users = CourseSessionUser.objects.filter( + agentparticipantrelation__agent=user, + agentparticipantrelation__participant__course_session=root.course_session_id, + ) + + grouped_course_session_users = groupby( + sorted(course_session_users, key=lambda x: x.created_at.year), + key=lambda x: x.created_at.year, + ) + + return [ + ParticipantsForYear( + _id=f"{root.course_session_id} {year}", + year=year, + participants=list(c), + ) + for year, c in grouped_course_session_users + ] diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py index 57250135..a9247d73 100644 --- a/server/vbv_lernwelt/dashboard/utils.py +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -98,7 +98,8 @@ def create_course_session_dict(course_session_object, my_role, user_role): def create_person_list_with_roles( user, course_session_ids=None, include_private_data=False ): - def create_user_dict(user_object): + def create_user_dict(csu: CourseSessionUser): + user_object = csu.user user_data = { "user_id": user_object.id, "first_name": user_object.first_name, @@ -107,6 +108,7 @@ def create_person_list_with_roles( "avatar_url_small": user_object.avatar_url_small, "avatar_url": user_object.avatar_url, "course_sessions": [], + "chosen_profile": csu.chosen_profile.code if csu.chosen_profile else "all", } if include_private_data: user_data["phone_number"] = user_object.phone_number @@ -130,9 +132,7 @@ def create_person_list_with_roles( ).select_related("user") my_role = user_role(cs.roles) for csu in course_session_users: - person_data = result_persons.get( - csu.user.id, create_user_dict(csu.user) - ) + person_data = result_persons.get(csu.user.id, create_user_dict(csu)) person_data["course_sessions"].append( create_course_session_dict(cs, my_role, csu.role) ) @@ -146,7 +146,7 @@ def create_person_list_with_roles( participant_user = relation.participant.user if participant_user.id not in result_persons: - person_data = create_user_dict(participant_user) + person_data = create_user_dict(relation.participant) person_data["course_sessions"] = [course_session_entry] result_persons[participant_user.id] = person_data else: diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 3c605fd6..db5d5b80 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -62,6 +62,7 @@ class WidgetType(Enum): COMPETENCE_CERTIFICATE_WIDGET = "CompetenceCertificateWidget" UK_STATISTICS_WIDGET = "UKStatisticsWidget" UK_BERUFSBILDNER_STATISTICS_WIDGET = "UKBerufsbildnerStatisticsWidget" + TRAINING_RESPONSIBLE_STATISTICS_WIDGET = "TrainingResponsibleStatisticsWidget" class RoleKeyType(Enum): @@ -71,6 +72,7 @@ class RoleKeyType(Enum): SUPERVISOR = "Supervisor" TRAINER = "Trainer" BERUFSBILDNER = "Berufsbildner" + TRAINING_RESPONSIBLE = "Ausbildungsverantwortlicher" UNKNOWN_ROLE_KEY = "UnknownRoleKey" @@ -210,6 +212,8 @@ def get_widgets_for_course( if "BERUFSBILDNER" in relation_roles: if is_uk: widgets.append(WidgetType.UK_BERUFSBILDNER_STATISTICS_WIDGET.value) + if is_vv: + widgets.append(WidgetType.TRAINING_RESPONSIBLE_STATISTICS_WIDGET.value) return widgets @@ -237,6 +241,8 @@ def get_relevant_role_key( elif "BERUFSBILDNER" in relation_roles: if is_uk: return RoleKeyType.BERUFSBILDNER + elif is_vv: + return RoleKeyType.TRAINING_RESPONSIBLE return RoleKeyType.UNKNOWN_ROLE_KEY diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py index eac0bcd9..8305c5e8 100644 --- a/server/vbv_lernwelt/shop/views.py +++ b/server/vbv_lernwelt/shop/views.py @@ -37,6 +37,10 @@ PRODUCT_SKU_TO_COURSE_SESSION_ID = { VV_IT_PRODUCT_SKU: 3, # vv-it } +COURSE_SESSION_ID_TO_PRODUCT_SKU = { + id: sku for sku, id in PRODUCT_SKU_TO_COURSE_SESSION_ID.items() +} + @api_view(["POST"]) def transaction_webhook(request):
+ {{ $t("Teilnehmer") }} +
{{ entry?.year }}
+ {{ participantsForYear(entry?.year ?? 0) }} {{ $t("a.Teilnehmer") }} +
+ {{ formatCurrencyChfCentimes(entry?.total_cost) }} CHF +