From 054f2f001a09841258ba076b3cab0672000a9c33 Mon Sep 17 00:00:00 2001 From: Marius Schuh Date: Fri, 27 Feb 2026 13:33:03 +0100 Subject: [PATCH 1/3] fix integration tests --- graphql/statistics/fields.ts | 59 +++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/graphql/statistics/fields.ts b/graphql/statistics/fields.ts index c4104d563..6b75477e3 100644 --- a/graphql/statistics/fields.ts +++ b/graphql/statistics/fields.ts @@ -1,7 +1,13 @@ import { Role } from '../authorizations'; import { Arg, Authorized, Field, FieldResolver, Float, Int, ObjectType, Query, Resolver, Root } from 'type-graphql'; import { prisma } from '../../common/prisma'; -import { course_category_enum, dissolve_reason, pupil_screening_status_enum, student_screening_status_enum as ScreeningStatus } from '@prisma/client'; +import { + course_category_enum, + dissolve_reason, + dissolved_by_enum, + pupil_screening_status_enum, + student_screening_status_enum as ScreeningStatus, +} from '@prisma/client'; import { GraphQLInt, GraphQLString } from 'graphql'; import moment from 'moment-timezone'; @@ -888,34 +894,39 @@ export class StatisticsResolver { @FieldResolver((returns) => [DataWithTrends]) @Authorized(Role.ADMIN) - async dissolvedMatches(@Root() statistics: Statistics) { + async dissolvedMatches(@Root() statistics: Statistics, @Arg('dissolvedBy', () => dissolved_by_enum) dissolvedBy: dissolved_by_enum) { const selectedDuration = moment(statistics.to).diff(moment(statistics.from), 'days') + 1; // include start const averages: { average_matches: number; dissolve_reason: string }[] = await prisma.$queryRaw` SELECT AVG(value) AS average_matches, - "indDissolveReason" as dissolve_reason + "indDissolveReason" as dissolve_reason, + "otherDissolveReason" as other_dissolve_reason FROM ( SELECT COUNT(*)::INT AS value, "indDissolveReason", + "otherDissolveReason", date_part('year', "dissolvedAt"::date) AS year, date_part('month', "dissolvedAt"::date) AS month FROM "match", UNNEST("dissolveReasons") as "indDissolveReason" /* dissolveReasons is an array, we want to count every single reason each month. UNNEST splits a row with the array into several rows with the specific array entries */ WHERE dissolved = TRUE AND "dissolvedAt" >= '2022-01-01'::timestamp AND "dissolvedAt" < ${statistics.from}::timestamp + AND "dissolvedBy" = ${dissolvedBy}::dissolved_by_enum GROUP BY "year", "month", "indDissolveReason" ) AS dissolved_reasons - GROUP BY "indDissolveReason" + GROUP BY "indDissolveReason", "otherDissolveReason" ORDER BY average_matches; `; - const data: { value: number; reason: string }[] = await prisma.$queryRaw` + const data: { value: number; reason: string; otherReason: string }[] = await prisma.$queryRaw` SELECT count(*)::int as value, - "singleDissolveReason" as reason + "singleDissolveReason" as reason, + "otherDissolveReason" as "otherReason" FROM "match", UNNEST("dissolveReasons") as "singleDissolveReason" WHERE dissolved = TRUE AND "dissolvedAt" >= ${statistics.from}::timestamp AND "dissolvedAt" < ${statistics.to}::timestamp - GROUP BY "singleDissolveReason" + AND "dissolvedBy" = ${dissolvedBy}::dissolved_by_enum + GROUP BY "singleDissolveReason", "otherDissolveReason" ORDER BY "singleDissolveReason" DESC; `; @@ -1070,7 +1081,7 @@ export class StatisticsResolver { EXTRACT(YEAR FROM l."start")::int AS year, EXTRACT(MONTH FROM l."start")::int AS month, SUM( - l."duration" * CASE + l."duration" * CASE WHEN l."appointmentType" = 'match' THEN 1 ELSE COALESCE(array_length(l."participantIds", 1), 0) END @@ -1114,7 +1125,7 @@ export class StatisticsResolver { COUNT(CASE WHEN l_count >= ${minCompletedLectures} THEN m.id END) AS successful_matches FROM match m LEFT JOIN ( - SELECT + SELECT l."matchId", COUNT(*) AS l_count FROM lecture l @@ -1151,7 +1162,7 @@ export class StatisticsResolver { EXTRACT(MONTH FROM l."start")::int AS month, COUNT(DISTINCT m."id")::int AS value FROM match m - JOIN lecture l + JOIN lecture l ON l."matchId" = m."id" WHERE l."start" >= ${statistics.from}::timestamp AND l."start" <= ${statistics.to}::timestamp AND l."isCanceled" = FALSE @@ -1168,7 +1179,7 @@ export class StatisticsResolver { SELECT COUNT(DISTINCT m."id")::int AS value FROM match m - JOIN lecture l + JOIN lecture l ON l."matchId" = m."id" WHERE l."start" >= ${statistics.from}::timestamp AND l."start" <= ${statistics.to}::timestamp AND l."isCanceled" = FALSE @@ -1186,13 +1197,13 @@ export class StatisticsResolver { EXTRACT(MONTH FROM l."start")::int AS month, COUNT(DISTINCT p_id)::int AS value FROM lecture l - LEFT JOIN subcourse sc + LEFT JOIN subcourse sc ON sc."id" = l."subcourseId" - LEFT JOIN course c + LEFT JOIN course c ON c."id" = sc."courseId" CROSS JOIN LATERAL UNNEST( - CASE - WHEN c."name" LIKE '%Hausaufgabenhilfe%' + CASE + WHEN c."name" LIKE '%Hausaufgabenhilfe%' THEN l."joinedBy" ELSE l."participantIds" END @@ -1226,8 +1237,8 @@ export class StatisticsResolver { LEFT JOIN subcourse sc ON sc."id" = l."subcourseId" LEFT JOIN course c ON c."id" = sc."courseId" CROSS JOIN LATERAL UNNEST( - CASE - WHEN c."name" LIKE '%Hausaufgabenhilfe%' + CASE + WHEN c."name" LIKE '%Hausaufgabenhilfe%' THEN l."joinedBy" ELSE l."organizerIds" END @@ -1256,12 +1267,12 @@ export class StatisticsResolver { SELECT COUNT(DISTINCT p_id)::int AS value FROM lecture l - LEFT JOIN subcourse sc + LEFT JOIN subcourse sc ON sc."id" = l."subcourseId" - LEFT JOIN course c + LEFT JOIN course c ON c."id" = sc."courseId" CROSS JOIN LATERAL UNNEST( - CASE + CASE WHEN c."name" LIKE '%Hausaufgabenhilfe%' THEN l."joinedBy" ELSE l."participantIds" @@ -1290,13 +1301,13 @@ export class StatisticsResolver { SELECT COUNT(DISTINCT s_id)::int AS value FROM lecture l - LEFT JOIN subcourse sc + LEFT JOIN subcourse sc ON sc."id" = l."subcourseId" - LEFT JOIN course c + LEFT JOIN course c ON c."id" = sc."courseId" CROSS JOIN LATERAL UNNEST( - CASE - WHEN c."name" LIKE '%Hausaufgabenhilfe%' + CASE + WHEN c."name" LIKE '%Hausaufgabenhilfe%' THEN l."joinedBy" ELSE l."organizerIds" END From d1c00ca2affca3cf8a4f82b6aba66522291c2344 Mon Sep 17 00:00:00 2001 From: Marius Schuh Date: Fri, 27 Feb 2026 13:35:03 +0100 Subject: [PATCH 2/3] fix integration tests --- graphql/statistics/fields.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/statistics/fields.ts b/graphql/statistics/fields.ts index 6b75477e3..cb31dde83 100644 --- a/graphql/statistics/fields.ts +++ b/graphql/statistics/fields.ts @@ -894,7 +894,7 @@ export class StatisticsResolver { @FieldResolver((returns) => [DataWithTrends]) @Authorized(Role.ADMIN) - async dissolvedMatches(@Root() statistics: Statistics, @Arg('dissolvedBy', () => dissolved_by_enum) dissolvedBy: dissolved_by_enum) { + async dissolvedMatches(@Root() statistics: Statistics, @Arg('dissolvedBy') dissolvedBy: dissolved_by_enum) { const selectedDuration = moment(statistics.to).diff(moment(statistics.from), 'days') + 1; // include start const averages: { average_matches: number; dissolve_reason: string }[] = await prisma.$queryRaw` SELECT AVG(value) AS average_matches, From f795d26fff1a719174c52e328fed9be3fc909866 Mon Sep 17 00:00:00 2001 From: Marius Schuh Date: Fri, 27 Feb 2026 13:57:38 +0100 Subject: [PATCH 3/3] add dissolvedBy parameter to dissolvedMatches, return other dissolve reasons --- graphql/statistics/fields.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/graphql/statistics/fields.ts b/graphql/statistics/fields.ts index cb31dde83..0e52ad32c 100644 --- a/graphql/statistics/fields.ts +++ b/graphql/statistics/fields.ts @@ -896,7 +896,7 @@ export class StatisticsResolver { @Authorized(Role.ADMIN) async dissolvedMatches(@Root() statistics: Statistics, @Arg('dissolvedBy') dissolvedBy: dissolved_by_enum) { const selectedDuration = moment(statistics.to).diff(moment(statistics.from), 'days') + 1; // include start - const averages: { average_matches: number; dissolve_reason: string }[] = await prisma.$queryRaw` + const averages: { average_matches: number; dissolve_reason: string; other_dissolve_reason: string }[] = await prisma.$queryRaw` SELECT AVG(value) AS average_matches, "indDissolveReason" as dissolve_reason, "otherDissolveReason" as other_dissolve_reason @@ -911,7 +911,7 @@ export class StatisticsResolver { AND "dissolvedAt" >= '2022-01-01'::timestamp AND "dissolvedAt" < ${statistics.from}::timestamp AND "dissolvedBy" = ${dissolvedBy}::dissolved_by_enum - GROUP BY "year", "month", "indDissolveReason" + GROUP BY "year", "month", "indDissolveReason", "otherDissolveReason" ) AS dissolved_reasons GROUP BY "indDissolveReason", "otherDissolveReason" ORDER BY average_matches; @@ -930,8 +930,13 @@ export class StatisticsResolver { ORDER BY "singleDissolveReason" DESC; `; - return data.map(({ reason, value }) => { - const avg = averages.find((a) => a.dissolve_reason === reason)?.average_matches; + return data.map(({ reason, otherReason, value }) => { + const avg = averages.find((a) => { + if (reason === dissolve_reason.other && a.dissolve_reason === dissolve_reason.other) { + return a.other_dissolve_reason === otherReason; + } + return a.dissolve_reason === reason; + })?.average_matches; let trend: number; if (avg) { trend = value / ((avg / 30) * selectedDuration) - 1.0; @@ -940,7 +945,7 @@ export class StatisticsResolver { } // the average is an average over a month; we need an average for the selected number of days. return { - label: reason, + label: reason === dissolve_reason.other ? `Sonstiges: ${otherReason}` : reason, value, trend, };