From 9c54e052bb90eb1554408acf23b107084d414e94 Mon Sep 17 00:00:00 2001 From: Jonas Wilms Date: Sat, 10 Feb 2024 01:35:49 +0100 Subject: [PATCH 01/10] feat: Derive Achievements & Important Informations --- common/achievement/derive.ts | 74 ++++++++++++++++++++++++++++++++++++ graphql/user/fields.ts | 8 ++++ 2 files changed, 82 insertions(+) create mode 100644 common/achievement/derive.ts diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts new file mode 100644 index 000000000..8489d777c --- /dev/null +++ b/common/achievement/derive.ts @@ -0,0 +1,74 @@ +import { pupil as Pupil } from '@prisma/client'; +import { User, getPupil, getStudent } from '../user'; +// TODO: Fix import when other PR is in +import { Achievement } from '../../graphql/types/achievement'; +import { prisma } from '../prisma'; + +interface ImportantInformation { + title: string; + description: string; + navigateTo: string; +} + +interface DerivedInfo { + importantInformations: ImportantInformation[]; + achievements: Achievement[]; +} + +// Large parts of our user communication are event based, i.e. users get a notification for an appointment, +// or receive an achievement after taking some action +// +// Sometimes we however want to communicate what users "can do" or "should do next" - This is usually easier to +// derive from the state in the database than to somehow represent this as complex event based state machines +// Depending on whether it is a "standalone step" or part of a "sequence of steps" we communicate these as important +// notifications or as "derived achievements" - unlike other achievements these are not tracked in the database but can be +// derived from the rest of the database on demand +export async function deriveAchievements(user: User): Promise { + const result: DerivedInfo = { importantInformations: [], achievements: [] }; + + if (user.pupilId) { + const pupil = await getPupil(user); + + // await derivePupilOnboarding(pupil, result); + await derivePupilMatching(pupil, result); + } + + if (user.studentId) { + const student = await getStudent(user); + + // await deriveStudentOnboarding(student, result); + // await deriveStudentMatching(student, result); + // ... + } + + return result; +} + +async function derivePupilMatching(pupil: Pupil, result: DerivedInfo) { + const hasRequest = pupil.openMatchRequestCount > 0; + const hasOpenScreening = (await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'pending', invalidated: false } })) > 0; + + if (!hasRequest && !hasOpenScreening) return; + + if (!hasRequest && hasOpenScreening) { + result.importantInformations.push({ + title: 'Einladung zum Screening', + description: '...', + navigateTo: '/...', + }); + } + + const hasSuccessfulScreening = await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'success', invalidated: false } }); + if (hasSuccessfulScreening) { + result.achievements.push({ + subtitle: 'Warten auf Match', + steps: [{ name: 'Screening absolviert' }, { name: 'Warte auf Match' }], + // ... + } as any); + } else { + result.achievements.push({ + subtitle: 'Screening absolvieren', + // ... + } as any); + } +} diff --git a/graphql/user/fields.ts b/graphql/user/fields.ts index aec56885f..b0650a743 100644 --- a/graphql/user/fields.ts +++ b/graphql/user/fields.ts @@ -10,6 +10,7 @@ import { Log, Push_subscription as PushSubscription, notification_channel_enum as NotificationChannelEnum, + Important_information, } from '../generated'; import { Root, Authorized, FieldResolver, Query, Resolver, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql'; import { GraphQLContext } from '../context'; @@ -34,6 +35,7 @@ import assert from 'assert'; import { getPushSubscriptions, publicKey } from '../../common/notification/channels/push'; import { getUserNotificationPreferences } from '../../common/notification'; import { evaluateUserRoles } from '../../common/user/evaluate_roles'; +import { deriveAchievements } from '../../common/achievement/derive'; @ObjectType() export class UserContact implements UserContactType { @@ -292,6 +294,12 @@ export class UserFieldsResolver { return await getAppointmentsForUser(user, take, skip, cursor, direction); } + @FieldResolver((returns) => [Important_information]) + @Authorized(Role.ADMIN, Role.OWNER) + async importantInformations(@Ctx() context: GraphQLContext) { + return (await deriveAchievements(context.user)).importantInformations; + } + @FieldResolver((returns) => Boolean) @Authorized(Role.ADMIN, Role.OWNER) async hasAppointments(@Root() user: User): Promise { From b99a6741783952f873e82727e36f5d023b48ddd2 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Thu, 23 May 2024 17:31:30 +0200 Subject: [PATCH 02/10] always use ghost achievements for important information --- common/achievement/derive.ts | 118 ++++++++++++++++++++++++++--------- common/achievement/get.ts | 3 +- common/achievement/types.ts | 4 +- graphql/user/fields.ts | 15 ++++- 4 files changed, 106 insertions(+), 34 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index 8489d777c..fc1a1fa94 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -1,8 +1,13 @@ -import { pupil as Pupil } from '@prisma/client'; -import { User, getPupil, getStudent } from '../user'; +import { + pupil as Pupil, + achievement_action_type_enum as AchievementActionType, + achievement_template_for_enum as AchievementTemplateFor, + achievement_type_enum as AchievementType, +} from '@prisma/client'; +import { User, getPupil, getStudent, userForPupil } from '../user'; // TODO: Fix import when other PR is in -import { Achievement } from '../../graphql/types/achievement'; import { prisma } from '../prisma'; +import { achievement_with_template } from './types'; interface ImportantInformation { title: string; @@ -12,7 +17,7 @@ interface ImportantInformation { interface DerivedInfo { importantInformations: ImportantInformation[]; - achievements: Achievement[]; + achievements: achievement_with_template[]; } // Large parts of our user communication are event based, i.e. users get a notification for an appointment, @@ -30,7 +35,7 @@ export async function deriveAchievements(user: User): Promise { const pupil = await getPupil(user); // await derivePupilOnboarding(pupil, result); - await derivePupilMatching(pupil, result); + await derivePupilMatching(user, pupil, result); } if (user.studentId) { @@ -44,31 +49,88 @@ export async function deriveAchievements(user: User): Promise { return result; } -async function derivePupilMatching(pupil: Pupil, result: DerivedInfo) { +async function derivePupilMatching(user: User, pupil: Pupil, result: DerivedInfo) { const hasRequest = pupil.openMatchRequestCount > 0; - const hasOpenScreening = (await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'pending', invalidated: false } })) > 0; + // const hasOpenScreening = (await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'pending', invalidated: false } })) > 0; + const hasSuccessfulScreening = await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'success', invalidated: false } }); - if (!hasRequest && !hasOpenScreening) return; + // 1) Pupil has to be sucessfully screened + // 2) Check if they've created a new request - if (!hasRequest && hasOpenScreening) { - result.importantInformations.push({ - title: 'Einladung zum Screening', - description: '...', - navigateTo: '/...', - }); - } + // TODO: check if necessary + if (!hasSuccessfulScreening) return; - const hasSuccessfulScreening = await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'success', invalidated: false } }); - if (hasSuccessfulScreening) { - result.achievements.push({ - subtitle: 'Warten auf Match', - steps: [{ name: 'Screening absolviert' }, { name: 'Warte auf Match' }], - // ... - } as any); - } else { - result.achievements.push({ - subtitle: 'Screening absolvieren', - // ... - } as any); - } + result.achievements.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: 'pupil_new_match', + groupOrder: 1, + type: AchievementType.SEQUENTIAL, + image: '', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', + footer: '', + actionName: 'Anfrage stellen', + actionRedirectLink: null, + actionType: AchievementActionType.Action, + condition: 'false', // This will ensure that an evaluation will always fail + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: null, + }, + context: {}, + recordValue: null, + achievedAt: hasRequest ? new Date() : null, + relation: '', + }); + + result.achievements.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: { + id: -1, + templateFor: AchievementTemplateFor.Course, + group: 'pupil_new_match', + groupOrder: 1, + type: AchievementType.SEQUENTIAL, + image: '', + tagline: null, + title: '', + subtitle: null, + description: '', + footer: '', + actionName: null, + actionRedirectLink: null, + actionType: AchievementActionType.Action, + condition: '', + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: null, + }, + context: {}, + recordValue: null, + achievedAt: new Date(), + relation: '', + }); + result.achievements.push({ + subtitle: 'Warten auf Match', + steps: [{ name: 'Screening absolviert' }, { name: 'Warte auf Match' }], + // ... + } as any); } diff --git a/common/achievement/get.ts b/common/achievement/get.ts index 95b614b94..36b84e303 100644 --- a/common/achievement/get.ts +++ b/common/achievement/get.ts @@ -1,7 +1,7 @@ import { prisma } from '../prisma'; import { Prisma } from '@prisma/client'; import { User } from '../user'; -import { AchievementState, AchievementType, PublicAchievement, PublicStep } from './types'; +import { AchievementState, AchievementType, PublicAchievement, PublicStep, ThenArg } from './types'; import { getAchievementState, renderAchievementWithContext, transformPrismaJson } from './util'; import { getAchievementImageURL } from './util'; import { isDefined } from './util'; @@ -24,7 +24,6 @@ export async function getUserAchievementsWithTemplates(user: User) { }); return userAchievementsWithTemplates; } -type ThenArg = T extends PromiseLike ? U : T; export type achievements_with_template = ThenArg>; const getAchievementById = async (user: User, achievementId: number): Promise => { diff --git a/common/achievement/types.ts b/common/achievement/types.ts index 4c2de2e9a..03d0a078d 100644 --- a/common/achievement/types.ts +++ b/common/achievement/types.ts @@ -1,4 +1,4 @@ -import { achievement_action_type_enum, achievement_event, achievement_template, achievement_type_enum, lecture } from '@prisma/client'; +import { achievement_action_type_enum, achievement_event, achievement_type_enum, lecture } from '@prisma/client'; import { ActionID, SpecificNotificationContext } from '../notification/actions'; import { User } from '../user'; import { prisma } from '../prisma'; @@ -90,7 +90,7 @@ export type ActionEvent = { context: SpecificNotificationContext; }; -type ThenArg = T extends PromiseLike ? U : T; +export type ThenArg = T extends PromiseLike ? U : T; export type achievement_with_template = ThenArg>; export type AchievementToCheck = Pick; diff --git a/graphql/user/fields.ts b/graphql/user/fields.ts index b0650a743..f9dca7e81 100644 --- a/graphql/user/fields.ts +++ b/graphql/user/fields.ts @@ -35,7 +35,7 @@ import assert from 'assert'; import { getPushSubscriptions, publicKey } from '../../common/notification/channels/push'; import { getUserNotificationPreferences } from '../../common/notification'; import { evaluateUserRoles } from '../../common/user/evaluate_roles'; -import { deriveAchievements } from '../../common/achievement/derive'; +import { AchievementState } from '../../common/achievement/types'; @ObjectType() export class UserContact implements UserContactType { @@ -297,7 +297,18 @@ export class UserFieldsResolver { @FieldResolver((returns) => [Important_information]) @Authorized(Role.ADMIN, Role.OWNER) async importantInformations(@Ctx() context: GraphQLContext) { - return (await deriveAchievements(context.user)).importantInformations; + const achievements = await getUserAchievements(context.user); + return achievements + .filter((a) => a.achievementType === 'SEQUENTIAL' && a.achievementState === AchievementState.ACTIVE) + .map( + (a): Important_information => ({ + id: a.id, + description: a.description, + title: a.title, + recipients: 'students', + language: 'de', + }) + ); } @FieldResolver((returns) => Boolean) From f92837100b6627c83e9d97635b95d28a64a67acd Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Wed, 29 May 2024 12:14:02 +0200 Subject: [PATCH 03/10] create ghost achievements and ensure a transition to real ones --- common/achievement/create.ts | 4 +- common/achievement/derive.ts | 172 +++++++++++++++++++---------------- common/achievement/get.ts | 27 ++++-- common/achievement/metric.ts | 5 + common/achievement/util.ts | 4 +- common/match/create.ts | 3 + graphql/user/fields.ts | 1 + seed-db.ts | 113 +++++++++++++++++++++++ 8 files changed, 238 insertions(+), 91 deletions(-) diff --git a/common/achievement/create.ts b/common/achievement/create.ts index a4ac0b0e6..b5b24b8a6 100644 --- a/common/achievement/create.ts +++ b/common/achievement/create.ts @@ -67,7 +67,7 @@ async function _createAchievement(currentTemplate: achievem // Consequently, the screening process would trigger the creation of the Onboarding achievement, implying several steps that are inappropriate for the student. // Thus, the line below ensures that only the current or subsequent achievement steps are created, while others are automatically bypassed. // Note: +1 is added because the index is 0-based, while the groupOrder is 1-based. - if (nextStepIndex + 1 < currentTemplate.groupOrder) { + if (templatesForGroup[nextStepIndex].groupOrder + 1 < currentTemplate.groupOrder) { return null; } @@ -105,7 +105,7 @@ async function createNextUserAchievement( recordValue: nextStepTemplate.type === 'STREAK' ? 0 : null, achievedAt: null, }, - select: { id: true, userId: true, context: true, template: true, achievedAt: true, recordValue: true, relation: true }, + include: { template: true }, }); metrics.AchievementsCreated.inc({ id: createdUserAchievement.template.id.toString(), diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index fc1a1fa94..d6aed3946 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -3,22 +3,65 @@ import { achievement_action_type_enum as AchievementActionType, achievement_template_for_enum as AchievementTemplateFor, achievement_type_enum as AchievementType, + achievement_template, + pupil_screening_status_enum, } from '@prisma/client'; -import { User, getPupil, getStudent, userForPupil } from '../user'; +import { User, getPupil, getStudent } from '../user'; // TODO: Fix import when other PR is in import { prisma } from '../prisma'; import { achievement_with_template } from './types'; +import { getAchievementTemplates, TemplateSelectEnum } from './template'; -interface ImportantInformation { - title: string; - description: string; - navigateTo: string; -} - -interface DerivedInfo { - importantInformations: ImportantInformation[]; - achievements: achievement_with_template[]; -} +const GhostAchievements: { [key: string]: achievement_template } = { + pupil_new_match_1: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: 'pupil_new_match', + groupOrder: 1, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', + footer: null, + actionName: 'Anfrage stellen', + actionRedirectLink: '/matching', + actionType: AchievementActionType.Action, + condition: 'false', // This will ensure that an evaluation will always fail + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Anfrage stellen', + }, + pupil_new_match_2: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: 'pupil_new_match', + groupOrder: 2, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Fantastisch, deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können, möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es, die perfekte Person für dich zu finden und genau zu verstehen, was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!', + footer: null, + actionName: 'Termin buchen', + actionRedirectLink: 'https://calendly.com', + actionType: AchievementActionType.Action, + condition: 'false', + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', + }, +}; // Large parts of our user communication are event based, i.e. users get a notification for an appointment, // or receive an achievement after taking some action @@ -28,14 +71,14 @@ interface DerivedInfo { // Depending on whether it is a "standalone step" or part of a "sequence of steps" we communicate these as important // notifications or as "derived achievements" - unlike other achievements these are not tracked in the database but can be // derived from the rest of the database on demand -export async function deriveAchievements(user: User): Promise { - const result: DerivedInfo = { importantInformations: [], achievements: [] }; +export async function deriveAchievements(user: User, realAchievements: achievement_with_template[]): Promise { + const result: achievement_with_template[] = []; if (user.pupilId) { const pupil = await getPupil(user); // await derivePupilOnboarding(pupil, result); - await derivePupilMatching(user, pupil, result); + await derivePupilMatching(user, pupil, result, realAchievements); } if (user.studentId) { @@ -49,88 +92,59 @@ export async function deriveAchievements(user: User): Promise { return result; } -async function derivePupilMatching(user: User, pupil: Pupil, result: DerivedInfo) { +export function deriveAchievementTemplates(group: string): achievement_template[] { + return Object.values(GhostAchievements).filter((row) => row.group === group); +} + +async function derivePupilMatching(user: User, pupil: Pupil, result: achievement_with_template[], userAchievements: achievement_with_template[]) { const hasRequest = pupil.openMatchRequestCount > 0; // const hasOpenScreening = (await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'pending', invalidated: false } })) > 0; - const hasSuccessfulScreening = await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'success', invalidated: false } }); - - // 1) Pupil has to be sucessfully screened - // 2) Check if they've created a new request + const hasSuccessfulScreening = await prisma.pupil_screening.count({ + where: { pupilId: pupil.id, status: pupil_screening_status_enum.success, invalidated: false }, + }); - // TODO: check if necessary - if (!hasSuccessfulScreening) return; + const userAchievement = userAchievements.find((row) => row.template.group === 'pupil_new_match'); + if (!userAchievement) { + const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); + if (!groups.has('pupil_new_match') || groups.get('pupil_new_match').length === 0) { + throw new Error('group template not found!'); + } + // If there is no real achievement yet, we have to fake the first one in the row as well + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: groups.get('pupil_new_match')[0], + context: {}, + recordValue: null, + achievedAt: null, + relation: null, + }); + } - result.achievements.push({ + result.push({ id: -1, templateId: -1, userId: user.userID, isSeen: true, - template: { - id: -1, - templateFor: AchievementTemplateFor.Match, - group: 'pupil_new_match', - groupOrder: 1, - type: AchievementType.SEQUENTIAL, - image: '', - tagline: 'Starte eine Lernpatenschaft', - title: 'Neue Lernunterstützung', - subtitle: null, - description: - 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', - footer: '', - actionName: 'Anfrage stellen', - actionRedirectLink: null, - actionType: AchievementActionType.Action, - condition: 'false', // This will ensure that an evaluation will always fail - conditionDataAggregations: {}, - isActive: true, - achievedDescription: null, - achievedFooter: null, - achievedImage: null, - sequentialStepName: null, - }, + template: GhostAchievements.pupil_new_match_1, context: {}, recordValue: null, - achievedAt: hasRequest ? new Date() : null, - relation: '', + achievedAt: hasRequest || userAchievement ? new Date() : null, + // achievedAt: new Date(), + relation: userAchievement?.relation ?? null, }); - result.achievements.push({ + result.push({ id: -1, templateId: -1, userId: user.userID, isSeen: true, - template: { - id: -1, - templateFor: AchievementTemplateFor.Course, - group: 'pupil_new_match', - groupOrder: 1, - type: AchievementType.SEQUENTIAL, - image: '', - tagline: null, - title: '', - subtitle: null, - description: '', - footer: '', - actionName: null, - actionRedirectLink: null, - actionType: AchievementActionType.Action, - condition: '', - conditionDataAggregations: {}, - isActive: true, - achievedDescription: null, - achievedFooter: null, - achievedImage: null, - sequentialStepName: null, - }, + template: GhostAchievements.pupil_new_match_2, context: {}, recordValue: null, - achievedAt: new Date(), - relation: '', + achievedAt: hasSuccessfulScreening || userAchievement ? new Date() : null, + relation: userAchievement?.relation ?? null, }); - result.achievements.push({ - subtitle: 'Warten auf Match', - steps: [{ name: 'Screening absolviert' }, { name: 'Warte auf Match' }], - // ... - } as any); } diff --git a/common/achievement/get.ts b/common/achievement/get.ts index 36b84e303..6c2f5a05b 100644 --- a/common/achievement/get.ts +++ b/common/achievement/get.ts @@ -7,14 +7,19 @@ import { getAchievementImageURL } from './util'; import { isDefined } from './util'; import { isAchievementConditionMet } from './evaluate'; import { getLogger } from '../logger/logger'; +import { deriveAchievements, deriveAchievementTemplates } from './derive'; const logger = getLogger('Achievement'); -export async function getUserAchievementsWithTemplates(user: User) { +export async function getUserAchievementsWithTemplates(user: User, byType: AchievementType | null = null) { + const templateSearch = { isActive: true }; + if (byType !== null) { + templateSearch['type'] = byType; + } const userAchievementsWithTemplates = await prisma.user_achievement.findMany({ where: { userId: user.userID, - template: { isActive: true }, + template: templateSearch, // This will ensure that we only get achievements that are either not streaks or have a recordValue of at least 1 // Otherwise, we would get all streaks that have not been started yet. // This can happen if an event related to a streak was emitted, but does not match any bucket, like "join on time". @@ -37,10 +42,10 @@ const getAchievementById = async (user: User, achievementId: number): Promise => { - const userAchievements = await prisma.user_achievement.findMany({ - where: { userId: user.userID, template: { type: AchievementType.SEQUENTIAL } }, - include: { template: true }, - }); + const userAchievements = await getUserAchievementsWithTemplates(user, AchievementType.SEQUENTIAL); + const derivedAchievements = await deriveAchievements(user, userAchievements); + userAchievements.push(...derivedAchievements); + const userAchievementGroups: { [groupRelation: string]: achievements_with_template } = {}; userAchievements.forEach((ua) => { const key = ua.relation ? `${ua.template.group}/${ua.relation}` : ua.template.group; @@ -106,6 +111,9 @@ const getFurtherAchievements = async (user: User): Promise // User achievements are already started by the user and are either active or completed. const getUserAchievements = async (user: User): Promise => { const userAchievements = await getUserAchievementsWithTemplates(user); + const derivedAchievements = await deriveAchievements(user, userAchievements); + userAchievements.push(...derivedAchievements); + const userAchievementGroups: { [group: string]: achievements_with_template } = {}; userAchievements.forEach((ua) => { if (!userAchievementGroups[`${ua.template.group}/${ua.relation}`]) { @@ -151,10 +159,13 @@ const assembleAchievementData = async (userAchievements: achievements_with_templ let currentAchievementIndex = userAchievements.findIndex((ua) => !ua.achievedAt); currentAchievementIndex = currentAchievementIndex >= 0 ? currentAchievementIndex : userAchievements.length - 1; - const achievementTemplates = await prisma.achievement_template.findMany({ + let achievementTemplates = await prisma.achievement_template.findMany({ where: { group: userAchievements[currentAchievementIndex].template.group, isActive: true }, orderBy: { groupOrder: 'asc' }, }); + const derivedTemplates = deriveAchievementTemplates(userAchievements[currentAchievementIndex].template.group); + achievementTemplates = [...achievementTemplates, ...derivedTemplates]; + achievementTemplates.sort((left, right) => left.groupOrder - right.groupOrder); let maxValue: number = achievementTemplates.length; let currentValue: number = currentAchievementIndex; @@ -191,7 +202,7 @@ const assembleAchievementData = async (userAchievements: achievements_with_templ maxValue = Object.keys(conditionDataAggregations).reduce((acc, key) => acc + conditionDataAggregations[key].valueToAchieve, 0); } - const state: AchievementState = getAchievementState(userAchievements, currentAchievementIndex); + const state: AchievementState = getAchievementState(userAchievements, currentAchievementIndex, achievementTemplates); const isNewAchievement = state === AchievementState.COMPLETED && !userAchievements[currentAchievementIndex].isSeen; const achievementContext = transformPrismaJson( diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index af674a235..d0073320e 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -104,6 +104,11 @@ const batchOfMetrics = [ createMetric('user_original_lern_fair', ['user_original_lern_fair'], () => { return 1; }), + + /* Matching */ + createMetric('pupil_match_create', ['tutee_matching_success'], () => { + return 1; + }), ]; export function registerAchievementMetrics() { diff --git a/common/achievement/util.ts b/common/achievement/util.ts index 3a443f3d5..830e101aa 100644 --- a/common/achievement/util.ts +++ b/common/achievement/util.ts @@ -166,10 +166,10 @@ export function renderAchievementWithContext( return currentAchievementContext as achievement_template; } -export function getAchievementState(userAchievements: user_achievement[], currentAchievementIndex: number) { +export function getAchievementState(userAchievements: user_achievement[], currentAchievementIndex: number, templates: achievement_template[]) { return userAchievements.length === 0 ? AchievementState.INACTIVE - : userAchievements[currentAchievementIndex].achievedAt + : userAchievements.length === templates.length && userAchievements[currentAchievementIndex].achievedAt ? AchievementState.COMPLETED : AchievementState.ACTIVE; } diff --git a/common/match/create.ts b/common/match/create.ts index 7a1feb902..edae8fd66 100644 --- a/common/match/create.ts +++ b/common/match/create.ts @@ -11,6 +11,7 @@ import type { ConcreteMatchPool } from './pool'; import { invalidateAllScreeningsOfPupil } from '../pupil/screening'; import { userForPupil, userForStudent } from '../user'; import { DAZ } from '../util/subjectsutils'; +import { createRelation, EventRelationType } from '../achievement/relation'; const logger = getLogger('Match'); @@ -79,6 +80,7 @@ export async function createMatch(pupil: Pupil, student: Student, pool: Concrete firstMatch: tutorFirstMatch, matchHash, matchDate, + relation: createRelation(EventRelationType.Match, match.id), }; await Notification.actionTaken(userForStudent(student), `tutor_matching_success`, tutorContext); @@ -99,6 +101,7 @@ export async function createMatch(pupil: Pupil, student: Student, pool: Concrete firstMatch: tuteeFirstMatch, matchHash, matchDate, + relation: createRelation(EventRelationType.Match, match.id), }; await Notification.actionTaken(userForPupil(pupil), `tutee_matching_${pool.name}`, tuteeContext); diff --git a/graphql/user/fields.ts b/graphql/user/fields.ts index f9dca7e81..10ece8cf9 100644 --- a/graphql/user/fields.ts +++ b/graphql/user/fields.ts @@ -307,6 +307,7 @@ export class UserFieldsResolver { title: a.title, recipients: 'students', language: 'de', + image: '', }) ); } diff --git a/seed-db.ts b/seed-db.ts index b97d5874c..5c542ada8 100644 --- a/seed-db.ts +++ b/seed-db.ts @@ -20,6 +20,9 @@ import { createPupilMatchRequest, createStudentMatchRequest } from './common/mat import { createCourseTag } from './common/courses/tags'; import { _setSilenceNotificationSystem } from './common/notification'; import { + achievement_action_type_enum, + achievement_template_for_enum, + achievement_type_enum, course_category_enum as CourseCategory, course_coursestate_enum as CourseState, course_subject_enum as CourseSubject, @@ -717,6 +720,116 @@ void (async function setupDevDB() { await importAchievements(); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 3, + sequentialStepName: 'Lernpartner:in erhalten', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: 'Starte eine Lernpatenschaft', + subtitle: null, + footer: null, + achievedFooter: null, + description: '', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Warten', + actionRedirectLink: null, + actionType: achievement_action_type_enum.Wait, + condition: 'pupil_match_create > 0', + conditionDataAggregations: JSON.parse('{"pupil_match_create":{"metric":"pupil_match_create","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 4, + sequentialStepName: 'Lernpartner:in kontaktieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉{{var:student.firstname}} ist super motiviert, dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen, kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: '{{var:student.firstname}} kontaktieren', + actionRedirectLink: '/chat', + actionType: achievement_action_type_enum.Action, + condition: 'student_verified_events > 0', + conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 5, + sequentialStepName: 'Erstes Gespräch absolvieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', + description: + 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', + achievedDescription: + 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Zum Termin', + actionRedirectLink: 'isso', + actionType: achievement_action_type_enum.Appointment, + condition: 'student_verified_events > 0', + conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + + // Add Instructors and Participants after adding Lectures, so that they are also added to the lectures: + await addSubcourseInstructor(null, subcourse1, student1); + await addSubcourseInstructor(null, subcourse1, student2); + + await addSubcourseInstructor(null, subcourse2, student1); + await joinSubcourse(subcourse2, pupil1, false); + await joinSubcourse(subcourse2, pupil2, false); + await joinSubcourse(subcourse2, pupil3, false); + await joinSubcourse(subcourse2, pupil4, false); + await joinSubcourse(subcourse2, pupil5, false); + await joinSubcourseWaitinglist(subcourse2, pupil6); + await joinSubcourseWaitinglist(subcourse2, pupil7); + + await addSubcourseInstructor(null, subcourse3, student1); + await addSubcourseInstructor(null, subcourse3, student2); + await joinSubcourse(subcourse3, pupil1, false); + await joinSubcourse(subcourse3, pupil2, false); + await joinSubcourse(subcourse3, pupil3, false); + + await addSubcourseInstructor(null, subcourse4, student2); + + await addSubcourseInstructor(null, subcourse5, student1); + await addSubcourseInstructor(null, subcourse5, student2); + await joinSubcourse(subcourse5, pupil1, false); + await joinSubcourse(subcourse5, pupil2, false); + await joinSubcourse(subcourse5, pupil3, false); + await joinSubcourse(subcourse5, pupil4, false); + await joinSubcourse(subcourse5, pupil5, false); + await joinSubcourse(subcourse5, pupil6, false); + await joinSubcourse(subcourse5, pupil7, false); + await joinSubcourse(subcourse5, pupil8, false); + await joinSubcourse(subcourse5, pupil9, false); + await joinSubcourse(subcourse5, pupil10, false); + if (!process.env.SKIP_NOTIFICATION_IMPORT) { await importNotificationsFromProd(); await importMessagesTranslationsFromProd(); From bc4f94376d6977ba933e2d7ddc58d070bf51d049 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Wed, 29 May 2024 12:58:02 +0200 Subject: [PATCH 04/10] remove achievement state hack --- common/achievement/get.ts | 6 +++--- common/achievement/util.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/achievement/get.ts b/common/achievement/get.ts index 6c2f5a05b..eccc84950 100644 --- a/common/achievement/get.ts +++ b/common/achievement/get.ts @@ -159,12 +159,12 @@ const assembleAchievementData = async (userAchievements: achievements_with_templ let currentAchievementIndex = userAchievements.findIndex((ua) => !ua.achievedAt); currentAchievementIndex = currentAchievementIndex >= 0 ? currentAchievementIndex : userAchievements.length - 1; - let achievementTemplates = await prisma.achievement_template.findMany({ + const achievementTemplates = await prisma.achievement_template.findMany({ where: { group: userAchievements[currentAchievementIndex].template.group, isActive: true }, orderBy: { groupOrder: 'asc' }, }); const derivedTemplates = deriveAchievementTemplates(userAchievements[currentAchievementIndex].template.group); - achievementTemplates = [...achievementTemplates, ...derivedTemplates]; + achievementTemplates.push(...derivedTemplates); achievementTemplates.sort((left, right) => left.groupOrder - right.groupOrder); let maxValue: number = achievementTemplates.length; @@ -202,7 +202,7 @@ const assembleAchievementData = async (userAchievements: achievements_with_templ maxValue = Object.keys(conditionDataAggregations).reduce((acc, key) => acc + conditionDataAggregations[key].valueToAchieve, 0); } - const state: AchievementState = getAchievementState(userAchievements, currentAchievementIndex, achievementTemplates); + const state: AchievementState = getAchievementState(userAchievements, currentAchievementIndex); const isNewAchievement = state === AchievementState.COMPLETED && !userAchievements[currentAchievementIndex].isSeen; const achievementContext = transformPrismaJson( diff --git a/common/achievement/util.ts b/common/achievement/util.ts index 830e101aa..3a443f3d5 100644 --- a/common/achievement/util.ts +++ b/common/achievement/util.ts @@ -166,10 +166,10 @@ export function renderAchievementWithContext( return currentAchievementContext as achievement_template; } -export function getAchievementState(userAchievements: user_achievement[], currentAchievementIndex: number, templates: achievement_template[]) { +export function getAchievementState(userAchievements: user_achievement[], currentAchievementIndex: number) { return userAchievements.length === 0 ? AchievementState.INACTIVE - : userAchievements.length === templates.length && userAchievements[currentAchievementIndex].achievedAt + : userAchievements[currentAchievementIndex].achievedAt ? AchievementState.COMPLETED : AchievementState.ACTIVE; } From ecfa94f66fd1fd90d89abcec553fe07c4e958910 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Wed, 29 May 2024 20:16:31 +0200 Subject: [PATCH 05/10] create idea of new important information format --- common/achievement/derive.ts | 6 ++++ graphql/user/fields.ts | 61 +++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index d6aed3946..c7d9052c1 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -104,6 +104,12 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement }); const userAchievement = userAchievements.find((row) => row.template.group === 'pupil_new_match'); + const matches = await prisma.match.findMany({ where: { pupilId: pupil.id } }); + + // if (!userAchievement && matches.length > 0) { + // return []; + // } + if (!userAchievement) { const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); if (!groups.has('pupil_new_match') || groups.get('pupil_new_match').length === 0) { diff --git a/graphql/user/fields.ts b/graphql/user/fields.ts index 10ece8cf9..847560798 100644 --- a/graphql/user/fields.ts +++ b/graphql/user/fields.ts @@ -12,7 +12,8 @@ import { notification_channel_enum as NotificationChannelEnum, Important_information, } from '../generated'; -import { Root, Authorized, FieldResolver, Query, Resolver, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql'; +import { Root, Authorized, FieldResolver, Query, Resolver, Arg, Ctx, ObjectType, Field, Int, createUnionType } from 'type-graphql'; +import { UNAUTHENTICATED_USER, loginAsUser } from '../authentication'; import { GraphQLContext } from '../context'; import { Role } from '../authorizations'; import { prisma } from '../../common/prisma'; @@ -58,6 +59,54 @@ export class Contact { @Field((_type) => String, { nullable: true }) chatId?: string; } + +@ObjectType() +export class ImportantInformationNew { + @Field((_type) => String) + title: string; + @Field((_type) => String) + description: string; + @Field((_type) => String) + type: 'normal' | 'sequence'; + + @Field((_type) => Int, { nullable: true }) + maxSteps: number | null; + @Field((_type) => Int, { nullable: true }) + finishedSteps: number | null; + // @Field((_type) => important_information_recipients_enum) + @Field((_type) => String) + recipients: 'students' | 'pupils'; + // @Field((_type) => important_information_language_enum) + @Field((_type) => String) + language: 'en' | 'de'; + @Field((_type) => ImportantInformationModal) + modal: typeof ImportantInformationModal; +} + +@ObjectType() +export class ImportantInformationTextModal { + @Field((_type) => String) + text: string; + @Field((_type) => String, { nullable: true }) + navigateTo: string | null; +} + +export const ImportantInformationAchievementModal = Achievement; + +export const ImportantInformationModal = createUnionType({ + name: 'Modal', + types: () => [ImportantInformationAchievementModal, ImportantInformationTextModal] as const, + resolveType: (value) => { + if ('tagline' in value) { + return ImportantInformationAchievementModal; + } + if ('text' in value) { + return ImportantInformationTextModal; + } + return undefined; + }, +}); + @Resolver((of) => UserType) export class UserFieldsResolver { @FieldResolver((returns) => String) @@ -294,20 +343,22 @@ export class UserFieldsResolver { return await getAppointmentsForUser(user, take, skip, cursor, direction); } - @FieldResolver((returns) => [Important_information]) + @FieldResolver((returns) => [ImportantInformationNew]) @Authorized(Role.ADMIN, Role.OWNER) async importantInformations(@Ctx() context: GraphQLContext) { const achievements = await getUserAchievements(context.user); return achievements .filter((a) => a.achievementType === 'SEQUENTIAL' && a.achievementState === AchievementState.ACTIVE) .map( - (a): Important_information => ({ - id: a.id, + (a): ImportantInformationNew => ({ description: a.description, title: a.title, recipients: 'students', language: 'de', - image: '', + type: 'sequence', + maxSteps: a.maxSteps, + finishedSteps: a.currentStep, + modal: a, }) ); } From bbf70c3d9904c8306a30e306559e379b1715a48f Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Sun, 2 Jun 2024 16:53:51 +0200 Subject: [PATCH 06/10] be able to create multiple ghost achievement rows --- common/achievement/derive.ts | 95 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index c7d9052c1..36ee05b10 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -5,18 +5,22 @@ import { achievement_type_enum as AchievementType, achievement_template, pupil_screening_status_enum, + Prisma, } from '@prisma/client'; -import { User, getPupil, getStudent } from '../user'; -// TODO: Fix import when other PR is in +import { User, getPupil } from '../user'; import { prisma } from '../prisma'; import { achievement_with_template } from './types'; import { getAchievementTemplates, TemplateSelectEnum } from './template'; +import { createRelation, EventRelationType } from './relation'; + +const PupilNewMatchGroup = 'pupil_new_match'; +const PupilNewMatchGroupOrder = 3; const GhostAchievements: { [key: string]: achievement_template } = { pupil_new_match_1: { id: -1, templateFor: AchievementTemplateFor.Match, - group: 'pupil_new_match', + group: PupilNewMatchGroup, groupOrder: 1, type: AchievementType.SEQUENTIAL, image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', @@ -40,7 +44,7 @@ const GhostAchievements: { [key: string]: achievement_template } = { pupil_new_match_2: { id: -1, templateFor: AchievementTemplateFor.Match, - group: 'pupil_new_match', + group: PupilNewMatchGroup, groupOrder: 2, type: AchievementType.SEQUENTIAL, image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', @@ -82,8 +86,7 @@ export async function deriveAchievements(user: User, realAchievements: achieveme } if (user.studentId) { - const student = await getStudent(user); - + // const student = await getStudent(user); // await deriveStudentOnboarding(student, result); // await deriveStudentMatching(student, result); // ... @@ -96,23 +99,19 @@ export function deriveAchievementTemplates(group: string): achievement_template[ return Object.values(GhostAchievements).filter((row) => row.group === group); } -async function derivePupilMatching(user: User, pupil: Pupil, result: achievement_with_template[], userAchievements: achievement_with_template[]) { - const hasRequest = pupil.openMatchRequestCount > 0; - // const hasOpenScreening = (await prisma.pupil_screening.count({ where: { pupilId: pupil.id, status: 'pending', invalidated: false } })) > 0; - const hasSuccessfulScreening = await prisma.pupil_screening.count({ - where: { pupilId: pupil.id, status: pupil_screening_status_enum.success, invalidated: false }, - }); - - const userAchievement = userAchievements.find((row) => row.template.group === 'pupil_new_match'); - const matches = await prisma.match.findMany({ where: { pupilId: pupil.id } }); - - // if (!userAchievement && matches.length > 0) { - // return []; - // } - - if (!userAchievement) { +async function generatePupilMatching( + achievement: achievement_with_template | null, + user: User, + hasRequest: boolean, + hasSuccessfulScreening: boolean, + ctx: PupilNewMatchGhostContext +): Promise { + const result: achievement_with_template[] = []; + // Generating a ramdom relation to be able to show multiple sequences of this kind in parallel + const randomRelation = createRelation(EventRelationType.Match, Math.random()) + '-tmp'; + if (!achievement) { const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); - if (!groups.has('pupil_new_match') || groups.get('pupil_new_match').length === 0) { + if (!groups.has(PupilNewMatchGroup) || groups.get(PupilNewMatchGroup).length === 0) { throw new Error('group template not found!'); } // If there is no real achievement yet, we have to fake the first one in the row as well @@ -121,11 +120,11 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement templateId: -1, userId: user.userID, isSeen: true, - template: groups.get('pupil_new_match')[0], - context: {}, + template: groups.get(PupilNewMatchGroup)[0], + context: ctx, recordValue: null, achievedAt: null, - relation: null, + relation: randomRelation, }); } @@ -135,11 +134,10 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement userId: user.userID, isSeen: true, template: GhostAchievements.pupil_new_match_1, - context: {}, + context: ctx, recordValue: null, - achievedAt: hasRequest || userAchievement ? new Date() : null, - // achievedAt: new Date(), - relation: userAchievement?.relation ?? null, + achievedAt: hasRequest || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, }); result.push({ @@ -148,9 +146,42 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement userId: user.userID, isSeen: true, template: GhostAchievements.pupil_new_match_2, - context: {}, + context: ctx, recordValue: null, - achievedAt: hasSuccessfulScreening || userAchievement ? new Date() : null, - relation: userAchievement?.relation ?? null, + achievedAt: hasSuccessfulScreening || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, }); + return result; +} + +interface PupilNewMatchGhostContext extends Prisma.JsonObject { + lastScreeningDate: string | null; +} + +async function derivePupilMatching(user: User, pupil: Pupil, result: achievement_with_template[], userAchievements: achievement_with_template[]) { + const hasRequest = pupil.openMatchRequestCount > 0; + const successfulScreenings = await prisma.pupil_screening.findMany({ + where: { pupilId: pupil.id, status: pupil_screening_status_enum.success, invalidated: false }, + orderBy: { createdAt: 'desc' }, + }); + + const newMatchAchievements = userAchievements.filter( + (row) => row.template.group === PupilNewMatchGroup && row.template.groupOrder === PupilNewMatchGroupOrder + ); + + const ctx: PupilNewMatchGhostContext = { + lastScreeningDate: null, + }; + if (successfulScreenings.length > 0) { + ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); + } + for (let i = 0; i < pupil.openMatchRequestCount; i++) { + const ghosts = await generatePupilMatching(null, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } + + for (const userAchievement of newMatchAchievements) { + const ghosts = await generatePupilMatching(userAchievement, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } } From 46dcd1574bd8f378a205024d330ead574c71960b Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Tue, 18 Jun 2024 20:00:17 +0200 Subject: [PATCH 07/10] derive student achievements --- common/achievement/derive.ts | 148 +++++++++++++++++++++++++++++++++-- common/achievement/metric.ts | 3 + seed-db.ts | 77 +++++++++++++++++- 3 files changed, 220 insertions(+), 8 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index 36ee05b10..e3130bbe4 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -1,5 +1,6 @@ import { pupil as Pupil, + student as Student, achievement_action_type_enum as AchievementActionType, achievement_template_for_enum as AchievementTemplateFor, achievement_type_enum as AchievementType, @@ -7,7 +8,7 @@ import { pupil_screening_status_enum, Prisma, } from '@prisma/client'; -import { User, getPupil } from '../user'; +import { User, getPupil, getStudent } from '../user'; import { prisma } from '../prisma'; import { achievement_with_template } from './types'; import { getAchievementTemplates, TemplateSelectEnum } from './template'; @@ -15,6 +16,8 @@ import { createRelation, EventRelationType } from './relation'; const PupilNewMatchGroup = 'pupil_new_match'; const PupilNewMatchGroupOrder = 3; +const StudentNewMatchGroup = 'student_new_match'; +const StudentNewMatchGroupOrder = 3; const GhostAchievements: { [key: string]: achievement_template } = { pupil_new_match_1: { @@ -65,6 +68,54 @@ const GhostAchievements: { [key: string]: achievement_template } = { achievedImage: null, sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', }, + student_new_match_1: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: StudentNewMatchGroup, + groupOrder: 1, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', + footer: null, + actionName: 'Anfrage stellen', + actionRedirectLink: '/matching', + actionType: AchievementActionType.Action, + condition: 'false', // This will ensure that an evaluation will always fail + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Anfrage stellen', + }, + student_new_match_2: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: StudentNewMatchGroup, + groupOrder: 2, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Fantastisch, deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können, möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es, die perfekte Person für dich zu finden und genau zu verstehen, was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!', + footer: null, + actionName: 'Termin buchen', + actionRedirectLink: 'https://calendly.com', + actionType: AchievementActionType.Action, + condition: 'false', + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', + }, }; // Large parts of our user communication are event based, i.e. users get a notification for an appointment, @@ -80,16 +131,12 @@ export async function deriveAchievements(user: User, realAchievements: achieveme if (user.pupilId) { const pupil = await getPupil(user); - - // await derivePupilOnboarding(pupil, result); await derivePupilMatching(user, pupil, result, realAchievements); } if (user.studentId) { - // const student = await getStudent(user); - // await deriveStudentOnboarding(student, result); - // await deriveStudentMatching(student, result); - // ... + const student = await getStudent(user); + await deriveStudentMatching(user, student, result, realAchievements); } return result; @@ -185,3 +232,90 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement result.push(...ghosts); } } + +interface StudentNewMatchGhostContext extends Prisma.JsonObject { + lastScreeningDate: string | null; +} + +async function deriveStudentMatching(user: User, student: Student, result: achievement_with_template[], userAchievements: achievement_with_template[]) { + const hasRequest = student.openMatchRequestCount > 0; + const successfulScreenings = await prisma.screening.findMany({ + where: { studentId: student.id, success: true }, + orderBy: { createdAt: 'desc' }, + }); + + const newMatchAchievements = userAchievements.filter( + (row) => row.template.group === StudentNewMatchGroup && row.template.groupOrder === StudentNewMatchGroupOrder + ); + + const ctx: StudentNewMatchGhostContext = { + lastScreeningDate: null, + }; + if (successfulScreenings.length > 0) { + ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); + } + for (let i = 0; i < student.openMatchRequestCount; i++) { + const ghosts = await generateStudentMatching(null, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } + + for (const userAchievement of newMatchAchievements) { + const ghosts = await generateStudentMatching(userAchievement, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } +} + +async function generateStudentMatching( + achievement: achievement_with_template | null, + user: User, + hasRequest: boolean, + hasSuccessfulScreening: boolean, + ctx: StudentNewMatchGhostContext +): Promise { + const result: achievement_with_template[] = []; + // Generating a ramdom relation to be able to show multiple sequences of this kind in parallel + const randomRelation = createRelation(EventRelationType.Match, Math.random()) + '-tmp'; + if (!achievement) { + const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); + if (!groups.has(StudentNewMatchGroup) || groups.get(StudentNewMatchGroup).length === 0) { + throw new Error('group template not found!'); + } + // If there is no real achievement yet, we have to fake the first one in the row as well + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: groups.get(StudentNewMatchGroup)[0], + context: ctx, + recordValue: null, + achievedAt: null, + relation: randomRelation, + }); + } + + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: GhostAchievements.student_new_match_1, + context: ctx, + recordValue: null, + achievedAt: hasRequest || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, + }); + + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: GhostAchievements.student_new_match_2, + context: ctx, + recordValue: null, + achievedAt: hasSuccessfulScreening || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, + }); + return result; +} diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index d0073320e..172ff0f65 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -109,6 +109,9 @@ const batchOfMetrics = [ createMetric('pupil_match_create', ['tutee_matching_success'], () => { return 1; }), + createMetric('student_match_create', ['tutor_matching_success'], () => { + return 1; + }), ]; export function registerAchievementMetrics() { diff --git a/seed-db.ts b/seed-db.ts index 5c542ada8..318bf50d0 100644 --- a/seed-db.ts +++ b/seed-db.ts @@ -764,6 +764,81 @@ void (async function setupDevDB() { actionName: '{{var:student.firstname}} kontaktieren', actionRedirectLink: '/chat', actionType: achievement_action_type_enum.Action, + condition: 'pupil_verified_events > 0', + conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 5, + sequentialStepName: 'Erstes Gespräch absolvieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', + description: + 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', + achievedDescription: + 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Zum Termin', + actionRedirectLink: 'isso', + actionType: achievement_action_type_enum.Appointment, + condition: 'pupil_verified_events > 0', + conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 3, + sequentialStepName: 'Lernpartner:in erhalten', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: 'Starte eine Lernpatenschaft', + subtitle: null, + footer: null, + achievedFooter: null, + description: '', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Warten', + actionRedirectLink: null, + actionType: achievement_action_type_enum.Wait, + condition: 'student_match_create > 0', + conditionDataAggregations: JSON.parse('{"student_match_create":{"metric":"student_match_create","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 4, + sequentialStepName: 'Lernpartner:in kontaktieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉{{var:student.firstname}} ist super motiviert, dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen, kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: '{{var:pupil.firstname}} kontaktieren', + actionRedirectLink: '/chat', + actionType: achievement_action_type_enum.Action, condition: 'student_verified_events > 0', conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), isActive: true, @@ -772,7 +847,7 @@ void (async function setupDevDB() { await prisma.achievement_template.create({ data: { templateFor: achievement_template_for_enum.Match, - group: 'pupil_new_match', + group: 'student_new_match', groupOrder: 5, sequentialStepName: 'Erstes Gespräch absolvieren', type: achievement_type_enum.SEQUENTIAL, From cfdb0b2d850522b06c22f858b670f444d1364a90 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Tue, 18 Jun 2024 20:02:55 +0200 Subject: [PATCH 08/10] remove important information user field --- graphql/user/fields.ts | 73 +----------------------------------------- 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/graphql/user/fields.ts b/graphql/user/fields.ts index 847560798..aec56885f 100644 --- a/graphql/user/fields.ts +++ b/graphql/user/fields.ts @@ -10,10 +10,8 @@ import { Log, Push_subscription as PushSubscription, notification_channel_enum as NotificationChannelEnum, - Important_information, } from '../generated'; -import { Root, Authorized, FieldResolver, Query, Resolver, Arg, Ctx, ObjectType, Field, Int, createUnionType } from 'type-graphql'; -import { UNAUTHENTICATED_USER, loginAsUser } from '../authentication'; +import { Root, Authorized, FieldResolver, Query, Resolver, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql'; import { GraphQLContext } from '../context'; import { Role } from '../authorizations'; import { prisma } from '../../common/prisma'; @@ -36,7 +34,6 @@ import assert from 'assert'; import { getPushSubscriptions, publicKey } from '../../common/notification/channels/push'; import { getUserNotificationPreferences } from '../../common/notification'; import { evaluateUserRoles } from '../../common/user/evaluate_roles'; -import { AchievementState } from '../../common/achievement/types'; @ObjectType() export class UserContact implements UserContactType { @@ -59,54 +56,6 @@ export class Contact { @Field((_type) => String, { nullable: true }) chatId?: string; } - -@ObjectType() -export class ImportantInformationNew { - @Field((_type) => String) - title: string; - @Field((_type) => String) - description: string; - @Field((_type) => String) - type: 'normal' | 'sequence'; - - @Field((_type) => Int, { nullable: true }) - maxSteps: number | null; - @Field((_type) => Int, { nullable: true }) - finishedSteps: number | null; - // @Field((_type) => important_information_recipients_enum) - @Field((_type) => String) - recipients: 'students' | 'pupils'; - // @Field((_type) => important_information_language_enum) - @Field((_type) => String) - language: 'en' | 'de'; - @Field((_type) => ImportantInformationModal) - modal: typeof ImportantInformationModal; -} - -@ObjectType() -export class ImportantInformationTextModal { - @Field((_type) => String) - text: string; - @Field((_type) => String, { nullable: true }) - navigateTo: string | null; -} - -export const ImportantInformationAchievementModal = Achievement; - -export const ImportantInformationModal = createUnionType({ - name: 'Modal', - types: () => [ImportantInformationAchievementModal, ImportantInformationTextModal] as const, - resolveType: (value) => { - if ('tagline' in value) { - return ImportantInformationAchievementModal; - } - if ('text' in value) { - return ImportantInformationTextModal; - } - return undefined; - }, -}); - @Resolver((of) => UserType) export class UserFieldsResolver { @FieldResolver((returns) => String) @@ -343,26 +292,6 @@ export class UserFieldsResolver { return await getAppointmentsForUser(user, take, skip, cursor, direction); } - @FieldResolver((returns) => [ImportantInformationNew]) - @Authorized(Role.ADMIN, Role.OWNER) - async importantInformations(@Ctx() context: GraphQLContext) { - const achievements = await getUserAchievements(context.user); - return achievements - .filter((a) => a.achievementType === 'SEQUENTIAL' && a.achievementState === AchievementState.ACTIVE) - .map( - (a): ImportantInformationNew => ({ - description: a.description, - title: a.title, - recipients: 'students', - language: 'de', - type: 'sequence', - maxSteps: a.maxSteps, - finishedSteps: a.currentStep, - modal: a, - }) - ); - } - @FieldResolver((returns) => Boolean) @Authorized(Role.ADMIN, Role.OWNER) async hasAppointments(@Root() user: User): Promise { From 308bdaa49c474ed1180c628174cc59ecbcc9a488 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Sun, 8 Sep 2024 18:49:04 +0200 Subject: [PATCH 09/10] create new achievements for first student match --- common/achievement/derive.ts | 73 +++----- common/achievement/get.ts | 9 +- common/achievement/metric.ts | 15 ++ common/appointment/create.ts | 8 + common/chat/create.ts | 18 ++ common/notification/actions.ts | 24 ++- scripts/parse-achievements/achievements.csv | 27 ++- scripts/parse-achievements/gql-enable.txt | 111 +++++++----- scripts/parse-achievements/gql-seed.txt | 161 +++++++++++++++++ seed-achievements.ts | 185 +++++++++++++++++++ seed-db.ts | 188 -------------------- 11 files changed, 516 insertions(+), 303 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index e3130bbe4..74fcc71ef 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -26,15 +26,15 @@ const GhostAchievements: { [key: string]: achievement_template } = { group: PupilNewMatchGroup, groupOrder: 1, type: AchievementType.SEQUENTIAL, - image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', + image: 'gamification/achievements/release/new_match/five_pieces/empty_state.png', tagline: 'Starte eine Lernpatenschaft', title: 'Neue Lernunterstützung', subtitle: null, description: - 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', + 'Damit wir dir den:die perfekte:n Lernpartner:in zuweisen können, musst du zunächst eine Anfrage auf unserer Plattform stellen. Dort kannst du ganz einfach die Fächer angeben, die für dich wichtig sind und in denen wir dir helfen können. Wir freuen uns darauf, mit dir gemeinsam durchzustarten und die Lernreise zu beginnen!', footer: null, actionName: 'Anfrage stellen', - actionRedirectLink: '/matching', + actionRedirectLink: '/request-match', actionType: AchievementActionType.Action, condition: 'false', // This will ensure that an evaluation will always fail conditionDataAggregations: {}, @@ -50,7 +50,7 @@ const GhostAchievements: { [key: string]: achievement_template } = { group: PupilNewMatchGroup, groupOrder: 2, type: AchievementType.SEQUENTIAL, - image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', + image: 'gamification/achievements/release/new_match/five_pieces/step_1.png', tagline: 'Starte eine Lernpatenschaft', title: 'Neue Lernunterstützung', subtitle: null, @@ -74,7 +74,7 @@ const GhostAchievements: { [key: string]: achievement_template } = { group: StudentNewMatchGroup, groupOrder: 1, type: AchievementType.SEQUENTIAL, - image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', + image: 'gamification/achievements/release/new_match/five_pieces/empty_state.png', tagline: 'Starte eine Lernpatenschaft', title: 'Neue Lernunterstützung', subtitle: null, @@ -82,7 +82,7 @@ const GhostAchievements: { [key: string]: achievement_template } = { 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', footer: null, actionName: 'Anfrage stellen', - actionRedirectLink: '/matching', + actionRedirectLink: '/request-match', actionType: AchievementActionType.Action, condition: 'false', // This will ensure that an evaluation will always fail conditionDataAggregations: {}, @@ -92,30 +92,6 @@ const GhostAchievements: { [key: string]: achievement_template } = { achievedImage: null, sequentialStepName: 'Anfrage stellen', }, - student_new_match_2: { - id: -1, - templateFor: AchievementTemplateFor.Match, - group: StudentNewMatchGroup, - groupOrder: 2, - type: AchievementType.SEQUENTIAL, - image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', - tagline: 'Starte eine Lernpatenschaft', - title: 'Neue Lernunterstützung', - subtitle: null, - description: - 'Fantastisch, deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können, möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es, die perfekte Person für dich zu finden und genau zu verstehen, was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!', - footer: null, - actionName: 'Termin buchen', - actionRedirectLink: 'https://calendly.com', - actionType: AchievementActionType.Action, - condition: 'false', - conditionDataAggregations: {}, - isActive: true, - achievedDescription: null, - achievedFooter: null, - achievedImage: null, - sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', - }, }; // Large parts of our user communication are event based, i.e. users get a notification for an appointment, @@ -175,6 +151,8 @@ async function generatePupilMatching( }); } + console.log('iso', hasRequest, hasSuccessfulScreening, achievement); + result.push({ id: -1, templateId: -1, @@ -211,6 +189,8 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement where: { pupilId: pupil.id, status: pupil_screening_status_enum.success, invalidated: false }, orderBy: { createdAt: 'desc' }, }); + const hasSuccessfulScreenings = successfulScreenings.length > 0; + const totalMatchCount = await prisma.match.count({ where: { pupilId: pupil.id } }); const newMatchAchievements = userAchievements.filter( (row) => row.template.group === PupilNewMatchGroup && row.template.groupOrder === PupilNewMatchGroupOrder @@ -222,13 +202,17 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement if (successfulScreenings.length > 0) { ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); } + // This case happens when the student just registered and had a successful screening + if (pupil.openMatchRequestCount === 0 && totalMatchCount === 0) { + const ghosts = await generatePupilMatching(null, user, hasRequest, hasSuccessfulScreenings, ctx); + result.push(...ghosts); + } for (let i = 0; i < pupil.openMatchRequestCount; i++) { - const ghosts = await generatePupilMatching(null, user, hasRequest, successfulScreenings.length > 0, ctx); + const ghosts = await generatePupilMatching(null, user, hasRequest, hasSuccessfulScreenings, ctx); result.push(...ghosts); } - for (const userAchievement of newMatchAchievements) { - const ghosts = await generatePupilMatching(userAchievement, user, hasRequest, successfulScreenings.length > 0, ctx); + const ghosts = await generatePupilMatching(userAchievement, user, hasRequest, hasSuccessfulScreenings, ctx); result.push(...ghosts); } } @@ -243,6 +227,7 @@ async function deriveStudentMatching(user: User, student: Student, result: achie where: { studentId: student.id, success: true }, orderBy: { createdAt: 'desc' }, }); + const totalMatchCount = await prisma.match.count({ where: { studentId: student.id } }); const newMatchAchievements = userAchievements.filter( (row) => row.template.group === StudentNewMatchGroup && row.template.groupOrder === StudentNewMatchGroupOrder @@ -254,13 +239,19 @@ async function deriveStudentMatching(user: User, student: Student, result: achie if (successfulScreenings.length > 0) { ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); } + // This case happens when the student just registered and had a successful screening + if (student.openMatchRequestCount === 0 && totalMatchCount === 0) { + const ghosts = await generateStudentMatching(null, user, hasRequest, ctx); + result.push(...ghosts); + } + // This will for (let i = 0; i < student.openMatchRequestCount; i++) { - const ghosts = await generateStudentMatching(null, user, hasRequest, successfulScreenings.length > 0, ctx); + const ghosts = await generateStudentMatching(null, user, hasRequest, ctx); result.push(...ghosts); } for (const userAchievement of newMatchAchievements) { - const ghosts = await generateStudentMatching(userAchievement, user, hasRequest, successfulScreenings.length > 0, ctx); + const ghosts = await generateStudentMatching(userAchievement, user, hasRequest, ctx); result.push(...ghosts); } } @@ -269,7 +260,6 @@ async function generateStudentMatching( achievement: achievement_with_template | null, user: User, hasRequest: boolean, - hasSuccessfulScreening: boolean, ctx: StudentNewMatchGhostContext ): Promise { const result: achievement_with_template[] = []; @@ -306,16 +296,5 @@ async function generateStudentMatching( relation: achievement?.relation ?? randomRelation, }); - result.push({ - id: -1, - templateId: -1, - userId: user.userID, - isSeen: true, - template: GhostAchievements.student_new_match_2, - context: ctx, - recordValue: null, - achievedAt: hasSuccessfulScreening || achievement ? new Date() : null, - relation: achievement?.relation ?? randomRelation, - }); return result; } diff --git a/common/achievement/get.ts b/common/achievement/get.ts index eccc84950..b2285c087 100644 --- a/common/achievement/get.ts +++ b/common/achievement/get.ts @@ -27,6 +27,10 @@ export async function getUserAchievementsWithTemplates(user: User, byType: Achie }, include: { template: true }, }); + + const derivedAchievements = await deriveAchievements(user, userAchievementsWithTemplates); + userAchievementsWithTemplates.push(...derivedAchievements); + return userAchievementsWithTemplates; } export type achievements_with_template = ThenArg>; @@ -43,8 +47,6 @@ const getAchievementById = async (user: User, achievementId: number): Promise => { const userAchievements = await getUserAchievementsWithTemplates(user, AchievementType.SEQUENTIAL); - const derivedAchievements = await deriveAchievements(user, userAchievements); - userAchievements.push(...derivedAchievements); const userAchievementGroups: { [groupRelation: string]: achievements_with_template } = {}; userAchievements.forEach((ua) => { @@ -111,9 +113,6 @@ const getFurtherAchievements = async (user: User): Promise // User achievements are already started by the user and are either active or completed. const getUserAchievements = async (user: User): Promise => { const userAchievements = await getUserAchievementsWithTemplates(user); - const derivedAchievements = await deriveAchievements(user, userAchievements); - userAchievements.push(...derivedAchievements); - const userAchievementGroups: { [group: string]: achievements_with_template } = {}; userAchievements.forEach((ua) => { if (!userAchievementGroups[`${ua.template.group}/${ua.relation}`]) { diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index 172ff0f65..cf1447700 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -29,6 +29,9 @@ const batchOfMetrics = [ createMetric('student_onboarding_screened', ['student_screening_appointment_done', 'tutor_screening_success', 'instructor_screening_success'], () => { return 1; }), + createMetric('student_onboarding_tutor_screened', ['tutor_screening_success'], () => { + return 1; + }), createMetric('student_onboarding_coc_success', ['student_coc_updated'], () => { return 1; }), @@ -49,6 +52,9 @@ const batchOfMetrics = [ ), /* CONDUCTED MATCH APPOINTMENT */ + createMetric('student_add_match_appointment', ['student_add_appointment_match_with_pupil'], () => { + return 1; + }), createMetric('student_conducted_match_appointment', ['student_joined_match_meeting'], () => { return 1; }), @@ -106,9 +112,18 @@ const batchOfMetrics = [ }), /* Matching */ + createMetric('pupil_create_new_match_chat', ['pupil_create_new_match_chat'], () => { + return 1; + }), + createMetric('student_create_new_match_chat', ['student_create_new_match_chat'], () => { + return 1; + }), createMetric('pupil_match_create', ['tutee_matching_success'], () => { return 1; }), + createMetric('student_match_requested', ['tutor_match_requested'], () => { + return 1; + }), createMetric('student_match_create', ['tutor_matching_success'], () => { return 1; }), diff --git a/common/appointment/create.ts b/common/appointment/create.ts index d19f9d3cb..0386393bd 100644 --- a/common/appointment/create.ts +++ b/common/appointment/create.ts @@ -14,6 +14,7 @@ import { getMatch, getPupil, getStudent } from '../../graphql/util'; import { PrerequisiteError, RedundantError } from '../../common/util/error'; import { getContextForGroupAppointmentReminder, getContextForMatchAppointmentReminder } from './util'; import { getNotificationContextForSubcourse } from '../../common/courses/notifications'; +import { createRelation, EventRelationType } from '../achievement/relation'; const logger = getLogger(); @@ -96,6 +97,7 @@ export const createMatchAppointments = async (matchId: number, appointmentsToBeC if (!silent) { await Notification.actionTaken(userForPupil(pupil), 'student_add_appointment_match', { + relation: createRelation(EventRelationType.Match, matchId), student, matchId: matchId.toString(), }); @@ -110,6 +112,12 @@ export const createMatchAppointments = async (matchId: number, appointmentsToBeC ...(await getContextForMatchAppointmentReminder(appointment)), pupil, }); + await Notification.actionTaken(userForStudent(student), 'student_add_appointment_match_with_pupil', { + relation: createRelation(EventRelationType.Match, matchId), + pupil, + match: { id: matchId.toString() }, + lecture: appointment, + }); } } diff --git a/common/chat/create.ts b/common/chat/create.ts index 3f20bc940..d2720cc5d 100644 --- a/common/chat/create.ts +++ b/common/chat/create.ts @@ -9,6 +9,8 @@ import systemMessages from './localization'; import { getLogger } from '../logger/logger'; import assert from 'assert'; import { createHmac } from 'crypto'; +import { actionTaken } from '../notification'; +import { createRelation, EventRelationType } from '../achievement/relation'; const logger = getLogger('Chat'); const getOrCreateOneOnOneConversation = async ( @@ -201,6 +203,22 @@ async function createContactChat(meUser: User, contactUser: User): Promise 0,"{""student_verified_events"":{""metric"":""student_onboarding_verified"";""aggregator"":""count""}}" Global,Sequential,student_onboarding,2,student,Onboarding,Willkommen bei Lern-Fair 👋,,Action,Termin buchen,https://calendly.com/lern-fair/huh-kennenlernen?email={{email}},Wir sind gespannt darauf; dich kennenzulernen! In einem kurzen; 15-minütigen Zoom-Gespräch möchten wir dir gerne unsere vielfältigen Engagement-Möglichkeiten vorstellen und alle deine Fragen beantworten. Buche einfach einen Termin; um mehr zu erfahren und dann voller Tatendrang direkt durchzustarten. Falls dir etwas dazwischen kommt; sage den Termin bitte ab und buche dir einen neuen.,,{{progress}} von {{maxValue}} Schritten abgeschlossen,Kennenlerngespräch absolvieren,{{progress}} von {{maxValue}} Schritten abgeschlossen,Flugticket,gamification/achievements/release/finish_onboarding/three_pieces/step_1.png,,student_screened_events > 0,"{""student_screened_events"":{""metric"":""student_onboarding_screened"";""aggregator"":""count""}}" Global,Sequential,student_onboarding,3,student,Onboarding,Willkommen bei Lern-Fair 👋,,Action,Infos zum Führungszeugnis,/certificate-of-conduct,Der Schutz von Kindern und Jugendlichen liegt uns sehr am Herzen; daher benötigen wir von allen Ehrenamtlichen ein erweitertes Führungszeugnis. Im nächsten Schritt findest du eine Anleitung zur Beantragung sowie eine Bescheinigung zur Kostenübernahme für das erweiterte Führungszeugnis. Um deinen Account aktiv zu halten; bitten wir dich; das erweiterte Führungszeugnis bis zum {{date}} bei uns einzureichen. Gemeinsam setzen wir uns für eine sichere Umgebung ein; in der alle sich wohl und geschützt fühlen können.,Herzlichen Glückwunsch! Du hast alle Onboarding-Schritte erfolgreich gemeistert und dir das Abflugticket für Loki gesichert. Wir sind begeistert; dass du nun Teil unseres Teams bist und Schüler:innen auf ihrem Lernweg begleitest. Gemeinsam setzen wir uns für eine bessere Bildung in Deutschland ein. Du bist bereits jetzt ein:e Lern-Fair Held:in! ❤️ Danke für dein Engagement und deine Begeisterung!,{{progress}} von {{maxValue}} Schritten abgeschlossen,Führungszeugnis einreichen,Wow! Du hast alle Schritte abgeschlossen.,Flugticket,gamification/achievements/release/finish_onboarding/three_pieces/step_2.png,gamification/achievements/release/finish_onboarding/three_pieces/step_3.png,student_coc_success_events > 0,"{""student_coc_success_events"":{""metric"":""student_onboarding_coc_success"";""aggregator"":""count""}}" -Match,Sequential,,1,student,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Action,Anfrage stellen,Match-Anfrage-Seite,Es war großartig; dich am {{date}} besser kennenzulernen und freuen uns; dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen; bitten wir dich zunächst; eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben; die für dich passend sind. Wir freuen uns auf den Start!,,,Anfrage stellen,,Rucksack,,,, -Match,Sequential,,2,student,Neue Lernunterstützung,Starte eine Lernpatenschaft,,None,,,"Wir sind mit vollem Engagement dabei; den:die ideale:n Lernpartner:in für dich zu finden.
 -Sobald wir jemanden entdeckt haben; der:die perfekt zu deinen angegebenen Fächern und Jahrgangsstufen passt; senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis zwei Wochen und hängt davon ab; wie gut deine Angaben und die Verfügbarkeit passender Lernpartner:innen übereinstimmen. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!",,,Schüler:in erhalten,,Rucksack,,,, -Match,Sequential,,3,student,Neue Lernunterstützung,{{name}},,Action,{{var:pupil.firstname}} kontaktieren,direkt auf den 1:1-Chat,"Hurra; wir haben eine:n Lernpartner:in für dich gefunden! 🎉 -{{var:pupil.firstname}} besucht die {{var:pupilGrade}} und ist gespannt darauf; gemeinsam mit dir in {{var:matchSubjects}} zu lernen. Trete über den Chat in Kontakt mit {{var:pupil.firstname}} und schlage ein erstes Kennenlerngespräch vor. Bitte habe Verständnis; falls nicht sofort eine Rückmeldung erfolgt – manche Schüler:innen überprüfen ihre Nachrichten nicht regelmäßig. Wir führen vor der Vermittlung Gespräche mit allen Schüler:innen; um ihre Bedürfnisse zu verstehen und überprüfen. {{var:pupil.firstname}} freut sich definitiv darauf; dich kennenzulernen!",,,Schüler:in kontaktieren,,Rucksack,,,, -Match,Sequential,,4,student,Neue Lernunterstützung,{{name}},,Action,Termin erstellen,Direkt in den Terminerstellungsprozess mit der Persno,"Starte dein Kennenlernen mit {{name}} auf unserer Plattform; indem du einen Termin erstellst. Unser System verknüpft deinen Termin automatisch mit einem Zoom-Meeting – Komfort pur! -Zusätzlich informieren wir {{name}} automatisch per E-Mail über neu geplante Termine und senden eine Erinnerung kurz vor dem Start des Treffens. Nutze diese Funktion auch zukünftig; um die Verlässlichkeit von {{name}} weiter zu stärken.",,,Termin erstellen,,Rucksack,,,, -Match,Sequential,,5,student,Neue Lernunterstützung,{{name}},,Appointment,Zum Termin,Link auf die Termin-Detailseite,"Wow; die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 -Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen; dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt; der dir hilfreiche Tipps; Tricks und spannende Gesprächsthemen bietet. Nutze ihn; um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!",Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher; dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!,,Erstes Gespräch absolvieren,Wow! Du hast alle Schritte abgeschlossen.,Rucksack,,,, +Match,Sequential,,1,student,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Action,Anfrage stellen,/request-match,Es war großartig; dich am {{date}} besser kennenzulernen und freuen uns; dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen; bitten wir dich zunächst; eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben; die für dich passend sind. Wir freuen uns auf den Start!,,,Anfrage stellen,,Rucksack,gamification/achievements/release/new_match/five_pieces/empty_state.png,,student_match_requested > 0,"{""student_match_requested"":{""metric"":""student_match_requested"";""aggregator"":""count""}}" +Match,Sequential,student_new_match,2,student,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Wait,Lernpartner:in erhalten,,Wir sind mit vollem Engagement dabei; den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben; der:die perfekt zu deinen angegebenen Fächern und Jahrgangsstufen passt; senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis zwei Wochen und hängt davon ab; wie gut deine Angaben und die Verfügbarkeit passender Lernpartner:innen übereinstimmen. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!,,,Schüler:in erhalten,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_1.png,,student_match_create > 0,"{""student_match_create"":{""metric"":""student_match_create"";""aggregator"":""count""}}" +Match,Sequential,student_new_match,3,student,Neue Lernunterstützung,{{pupil.firstname}},,Action,{{pupil.firstname}} kontaktieren,/chat,Hurra; wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{pupil.firstname}} besucht die {{pupil.grade}} und ist gespannt darauf; gemeinsam mit dir in {{matchSubjects}} zu lernen. Trete über den Chat in Kontakt mit {{pupil.firstname}} und schlage ein erstes Kennenlerngespräch vor. Bitte habe Verständnis; falls nicht sofort eine Rückmeldung erfolgt – manche Schüler:innen überprüfen ihre Nachrichten nicht regelmäßig. Wir führen vor der Vermittlung Gespräche mit allen Schüler:innen; um ihre Bedürfnisse zu verstehen und überprüfen. {{pupil.firstname}} freut sich definitiv darauf; dich kennenzulernen!,,,Schüler:in kontaktieren,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_2.png,,student_create_new_match_chat > 0,"{""student_create_new_match_chat"":{""metric"":""student_create_new_match_chat"";""aggregator"":""count""}}" +Match,Sequential,student_new_match,4,student,Neue Lernunterstützung,{{pupil.firstname}},,Action,Termin erstellen,/match/{{match.id}},Starte dein Kennenlernen mit {{pupil.firstname}} auf unserer Plattform; indem du einen Termin erstellst. Unser System verknüpft deinen Termin automatisch mit einem Zoom-Meeting – Komfort pur! Zusätzlich informieren wir {{pupil.firstname}} automatisch per E-Mail über neu geplante Termine und senden eine Erinnerung kurz vor dem Start des Treffens. Nutze diese Funktion auch zukünftig; um die Verlässlichkeit von {{pupil.firstname}} weiter zu stärken.,,,Termin erstellen,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_3.png,,student_add_match_appointment > 0,"{""student_add_match_appointment"":{""metric"":""student_add_match_appointment"";""aggregator"":""count""}}" +Match,Sequential,student_new_match,5,student,Neue Lernunterstützung,{{pupil.firstname}},,Appointment,Zum Termin,/appointment/{{lecture.id}},Wow; die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{pupil.firstname}} und hoffen; dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt; der dir hilfreiche Tipps; Tricks und spannende Gesprächsthemen bietet. Nutze ihn; um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!,Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{pupil.firstname}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher; dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!,,Erstes Gespräch absolvieren,Wow! Du hast alle Schritte abgeschlossen.,Rucksack,gamification/achievements/release/new_match/five_pieces/step_4.png,gamification/achievements/release/new_match/five_pieces/step_5.png,student_conducted_match_appointment > 0,"{""student_conducted_match_appointment"":{""metric"":""student_conducted_match_appointment"";""aggregator"":""count""}}" GlobalMatch,Tiered,student_conduct_match_appointment,1,student,1 durchgeführter Termin,1:1-Nachhilfe,,None,,,Sobald du einen Nachhilfe-Termin erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Super! Du hattest deinen ersten Nachhilfe-Termin mit deinem:r Lernpartner:in. Bleibt am Ball und setzt eure Lernreise motiviert fort. Genau wie unsere Eule Loki noch eine lange Reise vor sich hat; gibt es auch für deine:n Lernpartner:in noch vieles zu lernen. Wir hoffen; dass ihr beim gemeinsamen Lernen genauso viel Spaß haben werdet wie Loki beim Paddeln.,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Fluss,gamification/achievements/release/x_lectures_held/one_lectures_held.jpg,,student_match_appointments_count > 0,"{""student_match_appointments_count"":{""metric"":""student_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":1}}" GlobalMatch,Tiered,student_conduct_match_appointment,2,student,3 durchgeführte Termine,1:1-Nachhilfe,,None,,,Sobald du drei Nachhilfe-Termine erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Prima! Du hattest nun schon drei Nachhilfe-Termine. Schön; dass ihr eure Lernreise so fleißig startet. Lokis Reise führt heute vorbei an einem schönen; blühenden Kirschbaum. Auch der Baum hat einmal klein angefangen.Genauso wie der Baum gewachsen ist; wächst auch das Wissen deines:r Lernpartner:in mit jedem Termin in der Lernunterstützung. Also macht weiter so!,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Kirschblüte,gamification/achievements/release/x_lectures_held/three_lectures_held.jpg,,student_match_appointments_count > 2,"{""student_match_appointments_count"":{""metric"":""student_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":3}}" GlobalMatch,Tiered,student_conduct_match_appointment,3,student,5 durchgeführte Termine,1:1-Nachhilfe,,None,,,Sobald du fünf Nachhilfe-Termine erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Klasse! Fünf Nachhilfe-Termine in der Lernunterstützung habt ihr schon gemeinsam geschafft. Damit seid ihr auf eurer Lernreise schon weit gekommen. Loki schützt sich auf der Reise vor der Kälte in einem Iglu; das aus Eis gebaut wurde. Baut auch ihr weiter an dem Wissensschatz deiner:s Lernpartner:in; indem ihr weitere Termine in der Lernunterstützung absolviert. Tolle Arbeit; macht weiter!,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Iglu,gamification/achievements/release/x_lectures_held/five_lectures_held.jpg,,student_match_appointments_count > 4,"{""student_match_appointments_count"":{""metric"":""student_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":5}}" @@ -35,14 +31,11 @@ Global,Streak,student_appointment_reliability,1,student,Pünktlichkeits-Power,Zu Global,Streak,student_regular_learning,1,student,Login-Legende,User-Bereich,Du hast dich {{progress}} Monat(e) in Folge angemeldet!,None,,,Bleib dran und melde dich weiterhin jeden Monat auf unserer Plattform an; um deinen Streak zu verlängern. Regelmäßige Aktivität hilft dir dabei immer auf dem neuesten Stand zu bleiben. Du bist auf dem richtigen Weg – mach weiter so; du Anmelde-Champion!,,Noch {{remainingProgress}} Monat(e) mit Login bis zum neuen Rekord!,,Hurra; du erhöhst deinen Rekord weiter!,Eule,gamification/achievements/release/streaks/member_set.png,gamification/achievements/release/streaks/member_achieved.png,student_regular_learning > recordValue,"{""student_regular_learning"":{""metric"":""student_regular_learning"";""aggregator"":""last_streak_length"";""createBuckets"":""by_months"";""bucketAggregator"":""presence_of_events""}}" Global,Sequential,pupil_onboarding,1,pupil,Onboarding,Willkommen bei Lern-Fair 👋,,Action,E-Mail verifizieren,,Hurra! Am {{date}} haben wir eine E-Mail an deine Adresse {{email}} gesendet. Um deine E-Mail zu bestätigen; klicke einfach auf den Button in der Nachricht. Solltest du unsere E-Mail nicht finden; kannst du hier eine erneute Zustellung anfordern und voller Vorfreude auf unser Weiterkommen warten.,,,E-Mail verifizieren,{{progress}} von {{maxValue}} Schritten abgeschlossen,Flugticket,gamification/achievements/release/finish_onboarding/two_pieces/empty_state.png,,pupil_verified_events > 0,"{""pupil_verified_events"":{""metric"":""pupil_onboarding_verified"";""aggregator"":""count""}}" Global,Sequential,pupil_onboarding,2,pupil,Onboarding,Willkommen bei Lern-Fair 👋,,Action,Termin buchen,https://calendly.com/schueler-screening/willkommen-bei-lern-fair?email={{email}},Wir sind gespannt darauf; dich kennenzulernen! In einem kurzen; 15-minütigen Zoom-Gespräch möchten wir dir gerne unsere vielfältigen kostenlose Angebote vorstellen und dir die beste Unterstützung ermöglichen sowie alle deine Fragen beantworten. Buche einfach einen Termin; um mehr zu erfahren und dann voller Tatendrang direkt durchzustarten. Falls dir etwas dazwischen kommt; sage den Termin bitte ab und buche dir einen neuen.,Herzlichen Glückwunsch! Du hast alle Onboarding-Schritte erfolgreich gemeistert und dir das Abflugticket für Loki gesichert. Wir sind begeistert; dass du nun Teil unserer Lerncommunity bist und hoffen dich gut auf deiner Lernreise begleiten zu können. Loki und unser Team werden immer für dich da sein!,,Kennenlerngespräch absolvieren,{{progress}} von {{maxValue}} Schritten abgeschlossen,,gamification/achievements/release/finish_onboarding/two_pieces/step_1.png,gamification/achievements/release/finish_onboarding/two_pieces/step_2.png,pupil_screened_events > 0,"{""pupil_screened_events"":{""metric"":""pupil_onboarding_screened"";""aggregator"":""count""}}" -Match,Sequential,,1,pupil,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Action,Anfrage stellen,Match-Anfrage-Seite,Damit wir dir den:die perfekte:n Lernpartner:in zuweisen können; musst du zunächst eine Anfrage auf unserer Plattform stellen. Dort kannst du ganz einfach die Fächer angeben; die für dich wichtig sind und in denen wir dir helfen können. Wir freuen uns darauf; mit dir gemeinsam durchzustarten und die Lernreise zu beginnen!,,,Anfrage stellen,,Rucksack,,,, -Match,Sequential,,2,pupil,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Action,Termin buchen,Calendly SuS Zweitgespräch,Fantastisch; deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können; möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es; die perfekte Person für dich zu finden und genau zu verstehen; was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!,,,Gespräch mit Lern-Fair absolvieren,,Rucksack,,,, -Match,Sequential,,3,pupil,Neue Lernunterstützung,Starte eine Lernpatenschaft,,None,,,"Wir sind mit vollem Engagement dabei; den:die ideale:n Lernpartner:in für dich zu finden.
 -Sobald wir jemanden entdeckt haben; der:die perfekt zu deinen angegebenen Fächern passt; senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis vier Wochen und hängt davon ab; wie viele Lernpartner:innen derzeit für deine Fächer verfügbar sind. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!",,,Lernpartner:in erhalten,,Rucksack,,,, -Match,Sequential,,4,pupil,Neue Lernunterstützung,{{name}},,Action,{{var:student.firstname}} kontaktieren,direkt auf den 1:1-Chat,"Hurra; wir haben eine:n Lernpartner:in für dich gefunden! 🎉 -{{var:student.firstname}} ist super motiviert; dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen; kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten; dich kennenzulernen und gemeinsam mit dir durchzustarten!",,,Lernpartner:in kontaktieren,,Rucksack,,,, -Match,Sequential,,5,pupil,Neue Lernunterstützung,{{name}},,Appointment,Zum Termin,Link auf die Termin-Detailseite,"Wow; die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 -Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen; dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt; der dir hilfreiche Tipps; Tricks und spannende Gesprächsthemen bietet. Nutze ihn; um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!",Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher; dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!,,Erstes Gespräch absolvieren,Wow! Du hast alle Schritte abgeschlossen.,Rucksack,,,, +Match,Sequential,,1,pupil,Neues Lernunterstützung,Starte eine Lernpatenschaft,,Action,Anfrage stellen,/request-match,Damit wir dir den:die perfekte:n Lernpartner:in zuweisen können; musst du zunächst eine Anfrage auf unserer Plattform stellen. Dort kannst du ganz einfach die Fächer angeben; die für dich wichtig sind und in denen wir dir helfen können. Wir freuen uns darauf; mit dir gemeinsam durchzustarten und die Lernreise zu beginnen!,,,Anfrage stellen,,Rucksack,gamification/achievements/release/new_match/five_pieces/empty_state.png,,, +Match,Sequential,,2,pupil,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Action,Termin buchen,Calendly SuS Zweitgespräch,Fantastisch; deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können; möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es; die perfekte Person für dich zu finden und genau zu verstehen; was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!,,,Gespräch mit Lern-Fair absolvieren,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_1.png,,, +Match,Sequential,pupil_new_match,3,pupil,Neue Lernunterstützung,Starte eine Lernpatenschaft,,Wait,Lernpartner:in erhalten,,Wir sind mit vollem Engagement dabei; den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben; der:die perfekt zu deinen angegebenen Fächern passt; senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis vier Wochen und hängt davon ab; wie viele Lernpartner:innen derzeit für deine Fächer verfügbar sind. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!,,,Lernpartner:in erhalten,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_2.png,,pupil_match_create > 0,"{""pupil_match_create"":{""metric"":""pupil_match_create"";""aggregator"":""count""}}" +Match,Sequential,pupil_new_match,4,pupil,Neue Lernunterstützung,{{student.firstname}},,Action,{{student.firstname}} kontaktieren,/chat,Hurra; wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{student.firstname}} ist super motiviert; dir in {{matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{student.firstname}} loszulegen; kontaktieren {{student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{student.firstname}} kann es kaum erwarten; dich kennenzulernen und gemeinsam mit dir durchzustarten!,,,Lernpartner:in kontaktieren,,Rucksack,gamification/achievements/release/new_match/five_pieces/step_3.png,,pupil_create_new_match_chat > 0,"{""pupil_create_new_match_chat"":{""metric"":""pupil_create_new_match_chat"";""aggregator"":""count""}}" +Match,Sequential,pupil_new_match,5,pupil,Neue Lernunterstützung,{{student.firstname}},,Appointment,Zum Match,/match/{{match.id}},/match/{{match.id}},Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher; dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!,,Erstes Gespräch absolvieren,Wow! Du hast alle Schritte abgeschlossen.,Rucksack,gamification/achievements/release/new_match/five_pieces/step_4.png,gamification/achievements/release/new_match/five_pieces/step_5.png,pupil_conducted_match_appointment > 0,"{""pupil_conducted_match_appointment"":{""metric"":""pupil_conducted_match_appointment"";""aggregator"":""count""}}" GlobalMatch,Tiered,pupil_conduct_match_appointment,1,pupil,1 durchgeführter Termin,1:1-Nachhilfe,,None,,,Sobald du einen Nachhilfe-Termin erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Super! Du hattest deinen ersten Termin in der Lernunterstützung. Bleib am Ball und setze deine Lernreise motiviert fort. Genau wie unsere Eule Loki noch eine lange Reise vor sich hat; gibt es auch für dich noch vieles zu lernen. Wir hoffen; dass du beim Lernen genauso viel Spaß haben wirst wie Loki beim Paddeln.,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Fluss,gamification/achievements/release/x_lectures_held/one_lectures_held.jpg,,pupil_match_appointments_count > 0,"{""pupil_match_appointments_count"":{""metric"":""pupil_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":1}}" GlobalMatch,Tiered,pupil_conduct_match_appointment,2,pupil,3 durchgeführte Termine,1:1-Nachhilfe,,None,,,Sobald du drei Nachhilfe-Termine erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Prima! Du hast nun schon drei Termine in der Lernunterstützung gehabt. Schön; dass du deine Lernreise so fleißig startest. Lokis Reise führt heute vorbei an einem schönen; blühenden Kirschbaum. Auch der Baum hat einmal klein angefangen. Genauso wie der Baum gewachsen ist; wächst auch dein Wissen mit jedem Termin in der Lernunterstützung. Also mach weiter so!,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Kirschblüte,gamification/achievements/release/x_lectures_held/three_lectures_held.jpg,,pupil_match_appointments_count > 2,"{""pupil_match_appointments_count"":{""metric"":""pupil_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":3}}" GlobalMatch,Tiered,pupil_conduct_match_appointment,3,pupil,5 durchgeführte Termine,1:1-Nachhilfe,,None,,,Sobald du fünf Nachhilfe-Termine erfolgreich durchgeführt hast; geht die Reise mit unserer Eule Loki weiter. Bleib dran und schließe alle Termine ab; um zu sehen wohin euch die Lernreise als Nächstes führt.,Klasse! Fünf Termine in der Lernunterstützung hast du schon geschafft. Damit bist du auf deiner Lernreise schon weit gekommen. Loki schützt sich auf der Reise vor der Kälte in einem Iglu; das aus Eis gebaut wurde. Baue auch du weiter an deinem Wissensschatz; indem du weitere Termine in der Lernunterstützung machst. Tolle Arbeit; mach weiter!,{{progress}} von {{maxValue}} Terminen abgeschlossen,,Wow! Du hast alle Termine abgeschlossen.,Iglu,gamification/achievements/release/x_lectures_held/five_lectures_held.jpg,,pupil_match_appointments_count > 4,"{""pupil_match_appointments_count"":{""metric"":""pupil_conducted_match_appointment"";""aggregator"":""count"";""createBuckets"":""by_lecture_participation"";""bucketAggregator"":""presence_of_events"";""valueToAchieve"":5}}" diff --git a/scripts/parse-achievements/gql-enable.txt b/scripts/parse-achievements/gql-enable.txt index 3061a5a22..6379daa48 100644 --- a/scripts/parse-achievements/gql-enable.txt +++ b/scripts/parse-achievements/gql-enable.txt @@ -10,138 +10,159 @@ student_onboarding_2:achievementTemplateActivate(achievementTemplateId:2) student_onboarding_3:achievementTemplateActivate(achievementTemplateId:3) -student_conduct_match_appointment_1:achievementTemplateActivate(achievementTemplateId:4) +student_new_match_2:achievementTemplateActivate(achievementTemplateId:4) -student_conduct_match_appointment_2:achievementTemplateActivate(achievementTemplateId:5) +student_new_match_3:achievementTemplateActivate(achievementTemplateId:5) -student_conduct_match_appointment_3:achievementTemplateActivate(achievementTemplateId:6) +student_new_match_4:achievementTemplateActivate(achievementTemplateId:6) -student_conduct_match_appointment_4:achievementTemplateActivate(achievementTemplateId:7) +student_new_match_5:achievementTemplateActivate(achievementTemplateId:7) -student_conduct_match_appointment_5:achievementTemplateActivate(achievementTemplateId:8) +student_conduct_match_appointment_1:achievementTemplateActivate(achievementTemplateId:8) -student_conduct_match_appointment_6:achievementTemplateActivate(achievementTemplateId:9) +student_conduct_match_appointment_2:achievementTemplateActivate(achievementTemplateId:9) -student_conduct_match_appointment_7:achievementTemplateActivate(achievementTemplateId:10) +student_conduct_match_appointment_3:achievementTemplateActivate(achievementTemplateId:10) -student_conduct_course_appointment_1:achievementTemplateActivate(achievementTemplateId:11) +student_conduct_match_appointment_4:achievementTemplateActivate(achievementTemplateId:11) -student_conduct_course_appointment_2:achievementTemplateActivate(achievementTemplateId:12) +student_conduct_match_appointment_5:achievementTemplateActivate(achievementTemplateId:12) -student_conduct_course_appointment_3:achievementTemplateActivate(achievementTemplateId:13) +student_conduct_match_appointment_6:achievementTemplateActivate(achievementTemplateId:13) -student_conduct_course_appointment_4:achievementTemplateActivate(achievementTemplateId:14) +student_conduct_match_appointment_7:achievementTemplateActivate(achievementTemplateId:14) -student_conduct_course_appointment_5:achievementTemplateActivate(achievementTemplateId:15) +student_conduct_course_appointment_1:achievementTemplateActivate(achievementTemplateId:15) -student_conduct_course_appointment_6:achievementTemplateActivate(achievementTemplateId:16) +student_conduct_course_appointment_2:achievementTemplateActivate(achievementTemplateId:16) -student_conduct_course_appointment_7:achievementTemplateActivate(achievementTemplateId:17) +student_conduct_course_appointment_3:achievementTemplateActivate(achievementTemplateId:17) -student_offer_course_1:achievementTemplateActivate(achievementTemplateId:18) +student_conduct_course_appointment_4:achievementTemplateActivate(achievementTemplateId:18) -student_offer_course_2:achievementTemplateActivate(achievementTemplateId:19) +student_conduct_course_appointment_5:achievementTemplateActivate(achievementTemplateId:19) -student_offer_course_3:achievementTemplateActivate(achievementTemplateId:20) +student_conduct_course_appointment_6:achievementTemplateActivate(achievementTemplateId:20) -student_course_participation_1:achievementTemplateActivate(achievementTemplateId:21) +student_conduct_course_appointment_7:achievementTemplateActivate(achievementTemplateId:21) -student_course_participation_2:achievementTemplateActivate(achievementTemplateId:22) +student_offer_course_1:achievementTemplateActivate(achievementTemplateId:22) -student_match_regular_learning_1:achievementTemplateActivate(achievementTemplateId:23) +student_offer_course_2:achievementTemplateActivate(achievementTemplateId:23) -student_appointment_reliability_1:achievementTemplateActivate(achievementTemplateId:24) +student_offer_course_3:achievementTemplateActivate(achievementTemplateId:24) -student_regular_learning_1:achievementTemplateActivate(achievementTemplateId:25) +student_course_participation_1:achievementTemplateActivate(achievementTemplateId:25) -pupil_onboarding_1:achievementTemplateActivate(achievementTemplateId:26) +student_course_participation_2:achievementTemplateActivate(achievementTemplateId:26) -pupil_onboarding_2:achievementTemplateActivate(achievementTemplateId:27) +student_match_regular_learning_1:achievementTemplateActivate(achievementTemplateId:27) -pupil_conduct_match_appointment_1:achievementTemplateActivate(achievementTemplateId:28) +student_appointment_reliability_1:achievementTemplateActivate(achievementTemplateId:28) -pupil_conduct_match_appointment_2:achievementTemplateActivate(achievementTemplateId:29) +student_regular_learning_1:achievementTemplateActivate(achievementTemplateId:29) -pupil_conduct_match_appointment_3:achievementTemplateActivate(achievementTemplateId:30) +pupil_onboarding_1:achievementTemplateActivate(achievementTemplateId:30) -pupil_conduct_match_appointment_4:achievementTemplateActivate(achievementTemplateId:31) +pupil_onboarding_2:achievementTemplateActivate(achievementTemplateId:31) -pupil_conduct_match_appointment_5:achievementTemplateActivate(achievementTemplateId:32) +pupil_new_match_3:achievementTemplateActivate(achievementTemplateId:32) -pupil_conduct_match_appointment_6:achievementTemplateActivate(achievementTemplateId:33) +pupil_new_match_4:achievementTemplateActivate(achievementTemplateId:33) -pupil_conduct_match_appointment_7:achievementTemplateActivate(achievementTemplateId:34) +pupil_new_match_5:achievementTemplateActivate(achievementTemplateId:34) -pupil_conduct_course_appointment_1:achievementTemplateActivate(achievementTemplateId:35) +pupil_conduct_match_appointment_1:achievementTemplateActivate(achievementTemplateId:35) -pupil_conduct_course_appointment_2:achievementTemplateActivate(achievementTemplateId:36) +pupil_conduct_match_appointment_2:achievementTemplateActivate(achievementTemplateId:36) -pupil_conduct_course_appointment_3:achievementTemplateActivate(achievementTemplateId:37) +pupil_conduct_match_appointment_3:achievementTemplateActivate(achievementTemplateId:37) -pupil_conduct_course_appointment_4:achievementTemplateActivate(achievementTemplateId:38) +pupil_conduct_match_appointment_4:achievementTemplateActivate(achievementTemplateId:38) -pupil_conduct_course_appointment_5:achievementTemplateActivate(achievementTemplateId:39) +pupil_conduct_match_appointment_5:achievementTemplateActivate(achievementTemplateId:39) -pupil_conduct_course_appointment_6:achievementTemplateActivate(achievementTemplateId:40) +pupil_conduct_match_appointment_6:achievementTemplateActivate(achievementTemplateId:40) -pupil_conduct_course_appointment_7:achievementTemplateActivate(achievementTemplateId:41) +pupil_conduct_match_appointment_7:achievementTemplateActivate(achievementTemplateId:41) -pupil_course_participation_1:achievementTemplateActivate(achievementTemplateId:42) +pupil_conduct_course_appointment_1:achievementTemplateActivate(achievementTemplateId:42) -pupil_course_participation_2:achievementTemplateActivate(achievementTemplateId:43) +pupil_conduct_course_appointment_2:achievementTemplateActivate(achievementTemplateId:43) -pupil_match_regular_learning_1:achievementTemplateActivate(achievementTemplateId:44) +pupil_conduct_course_appointment_3:achievementTemplateActivate(achievementTemplateId:44) -pupil_appointment_reliability_1:achievementTemplateActivate(achievementTemplateId:45) +pupil_conduct_course_appointment_4:achievementTemplateActivate(achievementTemplateId:45) -pupil_regular_learning_1:achievementTemplateActivate(achievementTemplateId:46) +pupil_conduct_course_appointment_5:achievementTemplateActivate(achievementTemplateId:46) -user_original_corona_school_1:achievementTemplateActivate(achievementTemplateId:47) +pupil_conduct_course_appointment_6:achievementTemplateActivate(achievementTemplateId:47) -user_original_lern_fair_1:achievementTemplateActivate(achievementTemplateId:48) +pupil_conduct_course_appointment_7:achievementTemplateActivate(achievementTemplateId:48) + + +pupil_course_participation_1:achievementTemplateActivate(achievementTemplateId:49) + + +pupil_course_participation_2:achievementTemplateActivate(achievementTemplateId:50) + + +pupil_match_regular_learning_1:achievementTemplateActivate(achievementTemplateId:51) + + +pupil_appointment_reliability_1:achievementTemplateActivate(achievementTemplateId:52) + + +pupil_regular_learning_1:achievementTemplateActivate(achievementTemplateId:53) + + +user_original_corona_school_1:achievementTemplateActivate(achievementTemplateId:54) + + +user_original_lern_fair_1:achievementTemplateActivate(achievementTemplateId:55) } diff --git a/scripts/parse-achievements/gql-seed.txt b/scripts/parse-achievements/gql-seed.txt index 3f0a84f57..192d31775 100644 --- a/scripts/parse-achievements/gql-seed.txt +++ b/scripts/parse-achievements/gql-seed.txt @@ -70,6 +70,98 @@ student_onboarding_3: achievementTemplateCreate(data:{ }) +student_new_match_2: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "student_new_match", + groupOrder: 2, + sequentialStepName: "Schüler:in erhalten", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "Starte eine Lernpatenschaft", + subtitle: null, + footer: null, + achievedFooter: null, + description: "Wir sind mit vollem Engagement dabei, den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben, der:die perfekt zu deinen angegebenen Fächern und Jahrgangsstufen passt, senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis zwei Wochen und hängt davon ab, wie gut deine Angaben und die Verfügbarkeit passender Lernpartner:innen übereinstimmen. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!", + achievedDescription: null, + image: "gamification/achievements/release/new_match/five_pieces/step_1.png", + achievedImage: null, + actionName: "Lernpartner:in erhalten", + actionRedirectLink: null, + actionType: "Wait", + condition: "student_match_create > 0", + conditionDataAggregations: "{\"student_match_create\":{\"metric\":\"student_match_create\",\"aggregator\":\"count\"}}", +}) + + +student_new_match_3: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "student_new_match", + groupOrder: 3, + sequentialStepName: "Schüler:in kontaktieren", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "{{pupil.firstname}}", + subtitle: null, + footer: null, + achievedFooter: null, + description: "Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{pupil.firstname}} besucht die {{pupil.grade}} und ist gespannt darauf, gemeinsam mit dir in {{matchSubjects}} zu lernen. Trete über den Chat in Kontakt mit {{pupil.firstname}} und schlage ein erstes Kennenlerngespräch vor. Bitte habe Verständnis, falls nicht sofort eine Rückmeldung erfolgt – manche Schüler:innen überprüfen ihre Nachrichten nicht regelmäßig. Wir führen vor der Vermittlung Gespräche mit allen Schüler:innen, um ihre Bedürfnisse zu verstehen und überprüfen. {{pupil.firstname}} freut sich definitiv darauf, dich kennenzulernen!", + achievedDescription: null, + image: "gamification/achievements/release/new_match/five_pieces/step_2.png", + achievedImage: null, + actionName: "{{pupil.firstname}} kontaktieren", + actionRedirectLink: "/chat", + actionType: "Action", + condition: "student_create_new_match_chat > 0", + conditionDataAggregations: "{\"student_create_new_match_chat\":{\"metric\":\"student_create_new_match_chat\",\"aggregator\":\"count\"}}", +}) + + +student_new_match_4: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "student_new_match", + groupOrder: 4, + sequentialStepName: "Termin erstellen", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "{{pupil.firstname}}", + subtitle: null, + footer: null, + achievedFooter: null, + description: "Starte dein Kennenlernen mit {{pupil.firstname}} auf unserer Plattform, indem du einen Termin erstellst. Unser System verknüpft deinen Termin automatisch mit einem Zoom-Meeting – Komfort pur! Zusätzlich informieren wir {{pupil.firstname}} automatisch per E-Mail über neu geplante Termine und senden eine Erinnerung kurz vor dem Start des Treffens. Nutze diese Funktion auch zukünftig, um die Verlässlichkeit von {{pupil.firstname}} weiter zu stärken.", + achievedDescription: null, + image: "gamification/achievements/release/new_match/five_pieces/step_3.png", + achievedImage: null, + actionName: "Termin erstellen", + actionRedirectLink: "/match/{{match.id}}", + actionType: "Action", + condition: "student_add_match_appointment > 0", + conditionDataAggregations: "{\"student_add_match_appointment\":{\"metric\":\"student_add_match_appointment\",\"aggregator\":\"count\"}}", +}) + + +student_new_match_5: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "student_new_match", + groupOrder: 5, + sequentialStepName: "Erstes Gespräch absolvieren", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "{{pupil.firstname}}", + subtitle: null, + footer: null, + achievedFooter: "Wow! Du hast alle Schritte abgeschlossen.", + description: "Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{pupil.firstname}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!", + achievedDescription: "Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{pupil.firstname}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!", + image: "gamification/achievements/release/new_match/five_pieces/step_4.png", + achievedImage: "gamification/achievements/release/new_match/five_pieces/step_5.png", + actionName: "Zum Termin", + actionRedirectLink: "/appointment/{{lecture.id}}", + actionType: "Appointment", + condition: "student_conducted_match_appointment > 0", + conditionDataAggregations: "{\"student_conducted_match_appointment\":{\"metric\":\"student_conducted_match_appointment\",\"aggregator\":\"count\"}}", +}) + + student_conduct_match_appointment_1: achievementTemplateCreate(data:{ templateFor: "Global_Matches", group: "student_conduct_match_appointment", @@ -622,6 +714,75 @@ pupil_onboarding_2: achievementTemplateCreate(data:{ }) +pupil_new_match_3: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "pupil_new_match", + groupOrder: 3, + sequentialStepName: "Lernpartner:in erhalten", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "Starte eine Lernpatenschaft", + subtitle: null, + footer: null, + achievedFooter: null, + description: "Wir sind mit vollem Engagement dabei, den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben, der:die perfekt zu deinen angegebenen Fächern passt, senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis vier Wochen und hängt davon ab, wie viele Lernpartner:innen derzeit für deine Fächer verfügbar sind. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!", + achievedDescription: null, + image: "gamification/achievements/release/new_match/five_pieces/step_2.png", + achievedImage: null, + actionName: "Lernpartner:in erhalten", + actionRedirectLink: null, + actionType: "Wait", + condition: "pupil_match_create > 0", + conditionDataAggregations: "{\"pupil_match_create\":{\"metric\":\"pupil_match_create\",\"aggregator\":\"count\"}}", +}) + + +pupil_new_match_4: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "pupil_new_match", + groupOrder: 4, + sequentialStepName: "Lernpartner:in kontaktieren", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "{{student.firstname}}", + subtitle: null, + footer: null, + achievedFooter: null, + description: "Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{student.firstname}} ist super motiviert, dir in {{matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{student.firstname}} loszulegen, kontaktieren {{student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!", + achievedDescription: null, + image: "gamification/achievements/release/new_match/five_pieces/step_3.png", + achievedImage: null, + actionName: "{{student.firstname}} kontaktieren", + actionRedirectLink: "/chat", + actionType: "Action", + condition: "pupil_create_new_match_chat > 0", + conditionDataAggregations: "{\"pupil_create_new_match_chat\":{\"metric\":\"pupil_create_new_match_chat\",\"aggregator\":\"count\"}}", +}) + + +pupil_new_match_5: achievementTemplateCreate(data:{ + templateFor: "Match", + group: "pupil_new_match", + groupOrder: 5, + sequentialStepName: "Erstes Gespräch absolvieren", + type: "SEQUENTIAL", + title: "Neue Lernunterstützung", + tagline: "{{student.firstname}}", + subtitle: null, + footer: null, + achievedFooter: "Wow! Du hast alle Schritte abgeschlossen.", + description: "/match/{{match.id}}", + achievedDescription: "Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!", + image: "gamification/achievements/release/new_match/five_pieces/step_4.png", + achievedImage: "gamification/achievements/release/new_match/five_pieces/step_5.png", + actionName: "Zum Match", + actionRedirectLink: "/match/{{match.id}}", + actionType: "Appointment", + condition: "pupil_conducted_match_appointment > 0", + conditionDataAggregations: "{\"pupil_conducted_match_appointment\":{\"metric\":\"pupil_conducted_match_appointment\",\"aggregator\":\"count\"}}", +}) + + pupil_conduct_match_appointment_1: achievementTemplateCreate(data:{ templateFor: "Global_Matches", group: "pupil_conduct_match_appointment", diff --git a/seed-achievements.ts b/seed-achievements.ts index 30ba3ba62..9789ee32a 100644 --- a/seed-achievements.ts +++ b/seed-achievements.ts @@ -81,6 +81,113 @@ export async function importAchievements() { }, }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 2, + sequentialStepName: 'Schüler:in erhalten', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: 'Starte eine Lernpatenschaft', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Wir sind mit vollem Engagement dabei, den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben, der:die perfekt zu deinen angegebenen Fächern und Jahrgangsstufen passt, senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis zwei Wochen und hängt davon ab, wie gut deine Angaben und die Verfügbarkeit passender Lernpartner:innen übereinstimmen. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!', + achievedDescription: null, + image: 'gamification/achievements/release/new_match/five_pieces/step_1.png', + achievedImage: null, + actionName: 'Lernpartner:in erhalten', + actionRedirectLink: null, + actionType: achievement_action_type_enum.Wait, + condition: 'student_match_create > 0', + conditionDataAggregations: JSON.parse('{"student_match_create":{"metric":"student_match_create","aggregator":"count"}}'), + isActive: true, + }, + }); + + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 3, + sequentialStepName: 'Schüler:in kontaktieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{pupil.firstname}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{pupil.firstname}} besucht die {{pupil.grade}} und ist gespannt darauf, gemeinsam mit dir in {{matchSubjects}} zu lernen. Trete über den Chat in Kontakt mit {{pupil.firstname}} und schlage ein erstes Kennenlerngespräch vor. Bitte habe Verständnis, falls nicht sofort eine Rückmeldung erfolgt – manche Schüler:innen überprüfen ihre Nachrichten nicht regelmäßig. Wir führen vor der Vermittlung Gespräche mit allen Schüler:innen, um ihre Bedürfnisse zu verstehen und überprüfen. {{pupil.firstname}} freut sich definitiv darauf, dich kennenzulernen!', + achievedDescription: null, + image: 'gamification/achievements/release/new_match/five_pieces/step_2.png', + achievedImage: null, + actionName: '{{pupil.firstname}} kontaktieren', + actionRedirectLink: '/chat', + actionType: achievement_action_type_enum.Action, + condition: 'student_create_new_match_chat > 0', + conditionDataAggregations: JSON.parse('{"student_create_new_match_chat":{"metric":"student_create_new_match_chat","aggregator":"count"}}'), + isActive: true, + }, + }); + + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 4, + sequentialStepName: 'Termin erstellen', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{pupil.firstname}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Starte dein Kennenlernen mit {{pupil.firstname}} auf unserer Plattform, indem du einen Termin erstellst. Unser System verknüpft deinen Termin automatisch mit einem Zoom-Meeting – Komfort pur! Zusätzlich informieren wir {{pupil.firstname}} automatisch per E-Mail über neu geplante Termine und senden eine Erinnerung kurz vor dem Start des Treffens. Nutze diese Funktion auch zukünftig, um die Verlässlichkeit von {{pupil.firstname}} weiter zu stärken.', + achievedDescription: null, + image: 'gamification/achievements/release/new_match/five_pieces/step_3.png', + achievedImage: null, + actionName: 'Termin erstellen', + actionRedirectLink: '/match/{{match.id}}', + actionType: achievement_action_type_enum.Action, + condition: 'student_add_match_appointment > 0', + conditionDataAggregations: JSON.parse('{"student_add_match_appointment":{"metric":"student_add_match_appointment","aggregator":"count"}}'), + isActive: true, + }, + }); + + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 5, + sequentialStepName: 'Erstes Gespräch absolvieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{pupil.firstname}}', + subtitle: null, + footer: null, + achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', + description: + 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{pupil.firstname}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', + achievedDescription: + 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{pupil.firstname}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', + image: 'gamification/achievements/release/new_match/five_pieces/step_4.png', + achievedImage: 'gamification/achievements/release/new_match/five_pieces/step_5.png', + actionName: 'Zum Termin', + actionRedirectLink: '/appointment/{{lecture.id}}', + actionType: achievement_action_type_enum.Appointment, + condition: 'student_conducted_match_appointment > 0', + conditionDataAggregations: JSON.parse( + '{"student_conducted_match_appointment":{"metric":"student_conducted_match_appointment","aggregator":"count"}}' + ), + isActive: true, + }, + }); + await prisma.achievement_template.create({ data: { templateFor: achievement_template_for_enum.Global_Matches, @@ -757,6 +864,84 @@ export async function importAchievements() { }, }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 3, + sequentialStepName: 'Lernpartner:in erhalten', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: 'Starte eine Lernpatenschaft', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Wir sind mit vollem Engagement dabei, den:die ideale:n Lernpartner:in für dich zu finden. Sobald wir jemanden entdeckt haben, der:die perfekt zu deinen angegebenen Fächern passt, senden wir dir direkt eine E-Mail. Dieser Prozess dauert in der Regel nur ein bis vier Wochen und hängt davon ab, wie viele Lernpartner:innen derzeit für deine Fächer verfügbar sind. Deine Anfrage wurde am {{date}} gestellt – freue dich schon jetzt auf das kommende Lernabenteuer!', + achievedDescription: null, + image: 'gamification/achievements/release/new_match/five_pieces/step_2.png', + achievedImage: null, + actionName: 'Lernpartner:in erhalten', + actionRedirectLink: null, + actionType: achievement_action_type_enum.Wait, + condition: 'pupil_match_create > 0', + conditionDataAggregations: JSON.parse('{"pupil_match_create":{"metric":"pupil_match_create","aggregator":"count"}}'), + isActive: true, + }, + }); + + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 4, + sequentialStepName: 'Lernpartner:in kontaktieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{student.firstname}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉 {{student.firstname}} ist super motiviert, dir in {{matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{student.firstname}} loszulegen, kontaktieren {{student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', + achievedDescription: null, + image: 'gamification/achievements/release/new_match/five_pieces/step_3.png', + achievedImage: null, + actionName: '{{student.firstname}} kontaktieren', + actionRedirectLink: '/chat', + actionType: achievement_action_type_enum.Action, + condition: 'pupil_create_new_match_chat > 0', + conditionDataAggregations: JSON.parse('{"pupil_create_new_match_chat":{"metric":"pupil_create_new_match_chat","aggregator":"count"}}'), + isActive: true, + }, + }); + + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 5, + sequentialStepName: 'Erstes Gespräch absolvieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{student.firstname}}', + subtitle: null, + footer: null, + achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', + description: '/match/{{match.id}}', + achievedDescription: + 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', + image: 'gamification/achievements/release/new_match/five_pieces/step_4.png', + achievedImage: 'gamification/achievements/release/new_match/five_pieces/step_5.png', + actionName: 'Zum Match', + actionRedirectLink: '/match/{{match.id}}', + actionType: achievement_action_type_enum.Appointment, + condition: 'pupil_conducted_match_appointment > 0', + conditionDataAggregations: JSON.parse('{"pupil_conducted_match_appointment":{"metric":"pupil_conducted_match_appointment","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ data: { templateFor: achievement_template_for_enum.Global_Matches, diff --git a/seed-db.ts b/seed-db.ts index 318bf50d0..b97d5874c 100644 --- a/seed-db.ts +++ b/seed-db.ts @@ -20,9 +20,6 @@ import { createPupilMatchRequest, createStudentMatchRequest } from './common/mat import { createCourseTag } from './common/courses/tags'; import { _setSilenceNotificationSystem } from './common/notification'; import { - achievement_action_type_enum, - achievement_template_for_enum, - achievement_type_enum, course_category_enum as CourseCategory, course_coursestate_enum as CourseState, course_subject_enum as CourseSubject, @@ -720,191 +717,6 @@ void (async function setupDevDB() { await importAchievements(); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'pupil_new_match', - groupOrder: 3, - sequentialStepName: 'Lernpartner:in erhalten', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: 'Starte eine Lernpatenschaft', - subtitle: null, - footer: null, - achievedFooter: null, - description: '', - achievedDescription: null, - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: 'Warten', - actionRedirectLink: null, - actionType: achievement_action_type_enum.Wait, - condition: 'pupil_match_create > 0', - conditionDataAggregations: JSON.parse('{"pupil_match_create":{"metric":"pupil_match_create","aggregator":"count"}}'), - isActive: true, - }, - }); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'pupil_new_match', - groupOrder: 4, - sequentialStepName: 'Lernpartner:in kontaktieren', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: '{{name}}', - subtitle: null, - footer: null, - achievedFooter: null, - description: - 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉{{var:student.firstname}} ist super motiviert, dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen, kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', - achievedDescription: null, - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: '{{var:student.firstname}} kontaktieren', - actionRedirectLink: '/chat', - actionType: achievement_action_type_enum.Action, - condition: 'pupil_verified_events > 0', - conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), - isActive: true, - }, - }); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'pupil_new_match', - groupOrder: 5, - sequentialStepName: 'Erstes Gespräch absolvieren', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: '{{name}}', - subtitle: null, - footer: null, - achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', - description: - 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', - achievedDescription: - 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: 'Zum Termin', - actionRedirectLink: 'isso', - actionType: achievement_action_type_enum.Appointment, - condition: 'pupil_verified_events > 0', - conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), - isActive: true, - }, - }); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'student_new_match', - groupOrder: 3, - sequentialStepName: 'Lernpartner:in erhalten', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: 'Starte eine Lernpatenschaft', - subtitle: null, - footer: null, - achievedFooter: null, - description: '', - achievedDescription: null, - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: 'Warten', - actionRedirectLink: null, - actionType: achievement_action_type_enum.Wait, - condition: 'student_match_create > 0', - conditionDataAggregations: JSON.parse('{"student_match_create":{"metric":"student_match_create","aggregator":"count"}}'), - isActive: true, - }, - }); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'student_new_match', - groupOrder: 4, - sequentialStepName: 'Lernpartner:in kontaktieren', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: '{{name}}', - subtitle: null, - footer: null, - achievedFooter: null, - description: - 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉{{var:student.firstname}} ist super motiviert, dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen, kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', - achievedDescription: null, - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: '{{var:pupil.firstname}} kontaktieren', - actionRedirectLink: '/chat', - actionType: achievement_action_type_enum.Action, - condition: 'student_verified_events > 0', - conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), - isActive: true, - }, - }); - await prisma.achievement_template.create({ - data: { - templateFor: achievement_template_for_enum.Match, - group: 'student_new_match', - groupOrder: 5, - sequentialStepName: 'Erstes Gespräch absolvieren', - type: achievement_type_enum.SEQUENTIAL, - title: 'Neue Lernunterstützung', - tagline: '{{name}}', - subtitle: null, - footer: null, - achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', - description: - 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', - achievedDescription: - 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', - image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', - achievedImage: null, - actionName: 'Zum Termin', - actionRedirectLink: 'isso', - actionType: achievement_action_type_enum.Appointment, - condition: 'student_verified_events > 0', - conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), - isActive: true, - }, - }); - - // Add Instructors and Participants after adding Lectures, so that they are also added to the lectures: - await addSubcourseInstructor(null, subcourse1, student1); - await addSubcourseInstructor(null, subcourse1, student2); - - await addSubcourseInstructor(null, subcourse2, student1); - await joinSubcourse(subcourse2, pupil1, false); - await joinSubcourse(subcourse2, pupil2, false); - await joinSubcourse(subcourse2, pupil3, false); - await joinSubcourse(subcourse2, pupil4, false); - await joinSubcourse(subcourse2, pupil5, false); - await joinSubcourseWaitinglist(subcourse2, pupil6); - await joinSubcourseWaitinglist(subcourse2, pupil7); - - await addSubcourseInstructor(null, subcourse3, student1); - await addSubcourseInstructor(null, subcourse3, student2); - await joinSubcourse(subcourse3, pupil1, false); - await joinSubcourse(subcourse3, pupil2, false); - await joinSubcourse(subcourse3, pupil3, false); - - await addSubcourseInstructor(null, subcourse4, student2); - - await addSubcourseInstructor(null, subcourse5, student1); - await addSubcourseInstructor(null, subcourse5, student2); - await joinSubcourse(subcourse5, pupil1, false); - await joinSubcourse(subcourse5, pupil2, false); - await joinSubcourse(subcourse5, pupil3, false); - await joinSubcourse(subcourse5, pupil4, false); - await joinSubcourse(subcourse5, pupil5, false); - await joinSubcourse(subcourse5, pupil6, false); - await joinSubcourse(subcourse5, pupil7, false); - await joinSubcourse(subcourse5, pupil8, false); - await joinSubcourse(subcourse5, pupil9, false); - await joinSubcourse(subcourse5, pupil10, false); - if (!process.env.SKIP_NOTIFICATION_IMPORT) { await importNotificationsFromProd(); await importMessagesTranslationsFromProd(); From 85949f17f1994823bbcfb57f68db50be99fa7127 Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Mon, 30 Dec 2024 21:00:48 +0100 Subject: [PATCH 10/10] fix: typos --- common/achievement/derive.ts | 6 ++---- common/notification/actions.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index 74fcc71ef..036bdb435 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -151,8 +151,6 @@ async function generatePupilMatching( }); } - console.log('iso', hasRequest, hasSuccessfulScreening, achievement); - result.push({ id: -1, templateId: -1, @@ -202,7 +200,7 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement if (successfulScreenings.length > 0) { ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); } - // This case happens when the student just registered and had a successful screening + // This case happens when the pupil just registered and had a successful screening if (pupil.openMatchRequestCount === 0 && totalMatchCount === 0) { const ghosts = await generatePupilMatching(null, user, hasRequest, hasSuccessfulScreenings, ctx); result.push(...ghosts); @@ -263,7 +261,7 @@ async function generateStudentMatching( ctx: StudentNewMatchGhostContext ): Promise { const result: achievement_with_template[] = []; - // Generating a ramdom relation to be able to show multiple sequences of this kind in parallel + // Generating a random relation to be able to show multiple sequences of this kind in parallel const randomRelation = createRelation(EventRelationType.Match, Math.random()) + '-tmp'; if (!achievement) { const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); diff --git a/common/notification/actions.ts b/common/notification/actions.ts index 1877b0e39..88293bc4f 100644 --- a/common/notification/actions.ts +++ b/common/notification/actions.ts @@ -655,14 +655,14 @@ const _notificationActions = { }, }, pupil_create_new_match_chat: { - description: 'User has clicked on new chat with a match partner', + description: 'Tutee / Created a new chat with a match partner', sampleContext: { user: sampleUser, match: { id: '1' }, }, }, student_create_new_match_chat: { - description: 'User has clicked on new chat with a match partner', + description: 'Tutor / Created a new chat with a match partner', sampleContext: { user: sampleUser, match: { id: '1' },