diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index 2d9f4105f80..dc1753a6c7f 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -18,7 +18,8 @@ import { import { ObjectId } from 'bson'; import { GroupEntity, GroupEntityTypes } from '../../entity'; import { ClassRootType } from '../../uc/dto/class-root-type'; -import { ClassInfoSearchListResponse, ClassSortBy } from '../dto'; +import { ClassInfoSearchListResponse } from '../dto'; +import { ClassSortBy } from '../dto/interface'; const baseRouteName = '/groups'; @@ -120,6 +121,7 @@ describe('Group (API)', () => { name: group.name, externalSourceName: system.displayName, teachers: [adminUser.lastName], + studentCount: 0, }, { id: clazz.id, @@ -128,6 +130,7 @@ describe('Group (API)', () => { teachers: [teacherUser.lastName], schoolYear: schoolYear.name, isUpgradable: false, + studentCount: 0, }, ], skip: 0, diff --git a/apps/server/src/modules/group/controller/dto/interface/class-sort-by.enum.ts b/apps/server/src/modules/group/controller/dto/interface/class-sort-by.enum.ts new file mode 100644 index 00000000000..24dda3c9382 --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/interface/class-sort-by.enum.ts @@ -0,0 +1,4 @@ +export enum ClassSortBy { + NAME = 'name', + EXTERNAL_SOURCE_NAME = 'externalSourceName', +} diff --git a/apps/server/src/modules/group/controller/dto/interface/index.ts b/apps/server/src/modules/group/controller/dto/interface/index.ts new file mode 100644 index 00000000000..fa69bb70b30 --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/interface/index.ts @@ -0,0 +1,2 @@ +export * from './class-sort-by.enum'; +export * from './school-year-query-type.enum'; diff --git a/apps/server/src/modules/group/controller/dto/interface/school-year-query-type.enum.ts b/apps/server/src/modules/group/controller/dto/interface/school-year-query-type.enum.ts new file mode 100644 index 00000000000..ebec4637c46 --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/interface/school-year-query-type.enum.ts @@ -0,0 +1,5 @@ +export enum SchoolYearQueryType { + NEXT_YEAR = 'nextYear', + CURRENT_YEAR = 'currentYear', + PREVIOUS_YEARS = 'previousYears', +} diff --git a/apps/server/src/modules/group/controller/dto/request/class-filter-params.ts b/apps/server/src/modules/group/controller/dto/request/class-filter-params.ts new file mode 100644 index 00000000000..d6a7e3ba62f --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/request/class-filter-params.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { SchoolYearQueryType } from '../interface'; + +export class ClassFilterParams { + @IsOptional() + @IsEnum(SchoolYearQueryType) + @ApiPropertyOptional({ enum: SchoolYearQueryType, enumName: 'SchoolYearQueryType' }) + type?: SchoolYearQueryType; +} diff --git a/apps/server/src/modules/group/controller/dto/request/class-sort-params.ts b/apps/server/src/modules/group/controller/dto/request/class-sort-params.ts index 094f7efece4..980146c92d4 100644 --- a/apps/server/src/modules/group/controller/dto/request/class-sort-params.ts +++ b/apps/server/src/modules/group/controller/dto/request/class-sort-params.ts @@ -1,11 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { SortingParams } from '@shared/controller'; import { IsEnum, IsOptional } from 'class-validator'; - -export enum ClassSortBy { - NAME = 'name', - EXTERNAL_SOURCE_NAME = 'externalSourceName', -} +import { ClassSortBy } from '../interface'; export class ClassSortParams extends SortingParams { @IsOptional() diff --git a/apps/server/src/modules/group/controller/dto/request/index.ts b/apps/server/src/modules/group/controller/dto/request/index.ts index 17ecd658b7d..ceef988aa92 100644 --- a/apps/server/src/modules/group/controller/dto/request/index.ts +++ b/apps/server/src/modules/group/controller/dto/request/index.ts @@ -1,2 +1,3 @@ export * from './class-sort-params'; export * from './group-id-params'; +export * from './class-filter-params'; diff --git a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts index a62b8134158..c1e394174a6 100644 --- a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts +++ b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts @@ -23,6 +23,9 @@ export class ClassInfoResponse { @ApiPropertyOptional() isUpgradable?: boolean; + @ApiProperty() + studentCount: number; + constructor(props: ClassInfoResponse) { this.id = props.id; this.type = props.type; @@ -31,5 +34,6 @@ export class ClassInfoResponse { this.teachers = props.teachers; this.schoolYear = props.schoolYear; this.isUpgradable = props.isUpgradable; + this.studentCount = props.studentCount; } } diff --git a/apps/server/src/modules/group/controller/group.controller.ts b/apps/server/src/modules/group/controller/group.controller.ts index a7dc0c77563..c92ee337050 100644 --- a/apps/server/src/modules/group/controller/group.controller.ts +++ b/apps/server/src/modules/group/controller/group.controller.ts @@ -6,7 +6,7 @@ import { Page } from '@shared/domain'; import { ErrorResponse } from '@src/core/error/dto'; import { GroupUc } from '../uc'; import { ClassInfoDto, ResolvedGroupDto } from '../uc/dto'; -import { ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse } from './dto'; +import { ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse, ClassFilterParams } from './dto'; import { GroupResponseMapper } from './mapper'; @ApiTags('Group') @@ -23,11 +23,13 @@ export class GroupController { public async findClasses( @Query() pagination: PaginationParams, @Query() sortingQuery: ClassSortParams, + @Query() filterParams: ClassFilterParams, @CurrentUser() currentUser: ICurrentUser ): Promise { const board: Page = await this.groupUc.findAllClasses( currentUser.userId, currentUser.schoolId, + filterParams.type, pagination.skip, pagination.limit, sortingQuery.sortBy, diff --git a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts index 8c990cbd44a..668253de1ec 100644 --- a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts +++ b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts @@ -43,6 +43,7 @@ export class GroupResponseMapper { teachers: classInfo.teacherNames, schoolYear: classInfo.schoolYear, isUpgradable: classInfo.isUpgradable, + studentCount: classInfo.studentCount, }); return mapped; diff --git a/apps/server/src/modules/group/loggable/index.ts b/apps/server/src/modules/group/loggable/index.ts new file mode 100644 index 00000000000..0191fcaf981 --- /dev/null +++ b/apps/server/src/modules/group/loggable/index.ts @@ -0,0 +1 @@ +export * from './unknown-query-type-loggable-exception'; diff --git a/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.spec.ts b/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.spec.ts new file mode 100644 index 00000000000..c4f87e6a21a --- /dev/null +++ b/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { UnknownQueryTypeLoggableException } from './unknown-query-type-loggable-exception'; + +describe('UnknownQueryTypeLoggableException', () => { + describe('getLogMessage', () => { + const setup = () => { + const unknownQueryType = 'unknwon'; + + const exception = new UnknownQueryTypeLoggableException(unknownQueryType); + + return { + exception, + unknownQueryType, + }; + }; + + it('should log the correct message', () => { + const { exception, unknownQueryType } = setup(); + + const result = exception.getLogMessage(); + + expect(result).toEqual({ + type: 'INTERNAL_SERVER_ERROR', + stack: expect.any(String), + message: 'Unable to process unknown query type for class years.', + data: { + unknownQueryType, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.ts b/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.ts new file mode 100644 index 00000000000..758b4c1fb6b --- /dev/null +++ b/apps/server/src/modules/group/loggable/unknown-query-type-loggable-exception.ts @@ -0,0 +1,19 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { InternalServerErrorException } from '@nestjs/common'; + +export class UnknownQueryTypeLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly unknownQueryType: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'INTERNAL_SERVER_ERROR', + stack: this.stack, + message: 'Unable to process unknown query type for class years.', + data: { + unknownQueryType: this.unknownQueryType, + }, + }; + } +} diff --git a/apps/server/src/modules/group/uc/dto/class-info.dto.ts b/apps/server/src/modules/group/uc/dto/class-info.dto.ts index 611275e3bcd..c17689fe0fa 100644 --- a/apps/server/src/modules/group/uc/dto/class-info.dto.ts +++ b/apps/server/src/modules/group/uc/dto/class-info.dto.ts @@ -15,6 +15,8 @@ export class ClassInfoDto { isUpgradable?: boolean; + studentCount: number; + constructor(props: ClassInfoDto) { this.id = props.id; this.type = props.type; @@ -23,5 +25,6 @@ export class ClassInfoDto { this.teacherNames = props.teacherNames; this.schoolYear = props.schoolYear; this.isUpgradable = props.isUpgradable; + this.studentCount = props.studentCount; } } diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index d5236826def..51cf6151d45 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -28,6 +28,8 @@ import { GroupService } from '../service'; import { ClassInfoDto, ResolvedGroupDto } from './dto'; import { ClassRootType } from './dto/class-root-type'; import { GroupUc } from './group.uc'; +import { SchoolYearQueryType } from '../controller/dto/interface'; +import { UnknownQueryTypeLoggableException } from '../loggable'; describe('GroupUc', () => { let module: TestingModule; @@ -155,12 +157,26 @@ describe('GroupUc', () => { roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], }); const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + const nextSchoolYear: SchoolYearEntity = schoolYearFactory.buildWithId({ + startDate: schoolYear.endDate, + }); const clazz: Class = classFactory.build({ name: 'A', teacherIds: [teacherUser.id], source: 'LDAP', year: schoolYear.id, }); + const successorClass: Class = classFactory.build({ + name: 'NEW', + teacherIds: [teacherUser.id], + year: nextSchoolYear.id, + }); + const classWithoutSchoolYear = classFactory.build({ + name: 'NoYear', + teacherIds: [teacherUser.id], + year: undefined, + }); + const system: SystemDto = new SystemDto({ id: new ObjectId().toHexString(), displayName: 'External System', @@ -183,8 +199,10 @@ describe('GroupUc', () => { schoolService.getSchoolById.mockResolvedValueOnce(school); authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); authorizationService.hasAllPermissions.mockReturnValueOnce(false); - classService.findAllByUserId.mockResolvedValueOnce([clazz]); + classService.findAllByUserId.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); groupService.findByUser.mockResolvedValueOnce([group, groupWithSystem]); + classService.findClassesForSchool.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); + groupService.findClassesForSchool.mockResolvedValueOnce([group, groupWithSystem]); systemService.findById.mockResolvedValue(system); userService.findById.mockImplementation((userId: string): Promise => { if (userId === teacherUser.id) { @@ -208,23 +226,28 @@ describe('GroupUc', () => { throw new Error(); }); - schoolYearService.findById.mockResolvedValue(schoolYear); + schoolYearService.findById.mockResolvedValueOnce(schoolYear); + schoolYearService.findById.mockResolvedValueOnce(nextSchoolYear); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); return { teacherUser, school, clazz, + successorClass, + classWithoutSchoolYear, group, groupWithSystem, system, schoolYear, + nextSchoolYear, }; }; it('should check the required permissions', async () => { const { teacherUser, school } = setup(); - await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + await uc.findAllClasses(teacherUser.id, teacherUser.school.id, SchoolYearQueryType.CURRENT_YEAR); expect(authorizationService.checkPermission).toHaveBeenCalledWith<[User, LegacySchoolDo, AuthorizationContext]>( teacherUser, @@ -249,9 +272,19 @@ describe('GroupUc', () => { describe('when no pagination is given', () => { it('should return all classes sorted by name', async () => { - const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); - - const result: Page = await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + const { + teacherUser, + clazz, + successorClass, + classWithoutSchoolYear, + group, + groupWithSystem, + system, + schoolYear, + nextSchoolYear, + } = setup(); + + const result: Page = await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined); expect(result).toEqual>({ data: [ @@ -263,12 +296,37 @@ describe('GroupUc', () => { teacherNames: [teacherUser.lastName], schoolYear: schoolYear.name, isUpgradable: false, + studentCount: 2, + }, + { + id: successorClass.id, + name: successorClass.gradeLevel + ? `${successorClass.gradeLevel}${successorClass.name}` + : successorClass.name, + type: ClassRootType.CLASS, + externalSourceName: successorClass.source, + teacherNames: [teacherUser.lastName], + schoolYear: nextSchoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: classWithoutSchoolYear.id, + name: classWithoutSchoolYear.gradeLevel + ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` + : classWithoutSchoolYear.name, + type: ClassRootType.CLASS, + externalSourceName: classWithoutSchoolYear.source, + teacherNames: [teacherUser.lastName], + isUpgradable: false, + studentCount: 2, }, { id: group.id, name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, { id: groupWithSystem.id, @@ -276,20 +334,22 @@ describe('GroupUc', () => { type: ClassRootType.GROUP, externalSourceName: system.displayName, teacherNames: [teacherUser.lastName], + studentCount: 1, }, ], - total: 3, + total: 5, }); }); }); describe('when sorting by external source name in descending order', () => { it('should return all classes sorted by external source name in descending order', async () => { - const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); + const { teacherUser, clazz, classWithoutSchoolYear, group, groupWithSystem, system, schoolYear } = setup(); const result: Page = await uc.findAllClasses( teacherUser.id, teacherUser.school.id, + SchoolYearQueryType.CURRENT_YEAR, undefined, undefined, 'externalSourceName', @@ -298,6 +358,17 @@ describe('GroupUc', () => { expect(result).toEqual>({ data: [ + { + id: classWithoutSchoolYear.id, + name: classWithoutSchoolYear.gradeLevel + ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` + : classWithoutSchoolYear.name, + type: ClassRootType.CLASS, + externalSourceName: classWithoutSchoolYear.source, + teacherNames: [teacherUser.lastName], + isUpgradable: false, + studentCount: 2, + }, { id: clazz.id, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, @@ -306,6 +377,7 @@ describe('GroupUc', () => { teacherNames: [teacherUser.lastName], schoolYear: schoolYear.name, isUpgradable: false, + studentCount: 2, }, { id: groupWithSystem.id, @@ -313,15 +385,17 @@ describe('GroupUc', () => { type: ClassRootType.GROUP, externalSourceName: system.displayName, teacherNames: [teacherUser.lastName], + studentCount: 1, }, { id: group.id, name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, ], - total: 3, + total: 4, }); }); }); @@ -333,7 +407,8 @@ describe('GroupUc', () => { const result: Page = await uc.findAllClasses( teacherUser.id, teacherUser.school.id, - 1, + SchoolYearQueryType.CURRENT_YEAR, + 2, 1, 'name', SortOrder.asc @@ -346,12 +421,71 @@ describe('GroupUc', () => { name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, ], - total: 3, + total: 4, }); }); }); + + describe('when querying for classes from next school year', () => { + it('should only return classes from next school year', async () => { + const { teacherUser, successorClass, nextSchoolYear } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.NEXT_YEAR + ); + + expect(result).toEqual>({ + data: [ + { + id: successorClass.id, + name: successorClass.gradeLevel + ? `${successorClass.gradeLevel}${successorClass.name}` + : successorClass.name, + externalSourceName: successorClass.source, + type: ClassRootType.CLASS, + teacherNames: [teacherUser.lastName], + schoolYear: nextSchoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + ], + total: 1, + }); + }); + }); + + describe('when querying for archived classes', () => { + it('should only return classes from previous school years', async () => { + const { teacherUser } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.PREVIOUS_YEARS + ); + + expect(result).toEqual>({ + data: [], + total: 0, + }); + }); + }); + + describe('when querying for not existing type', () => { + it('should throw', async () => { + const { teacherUser } = setup(); + + const func = async () => + uc.findAllClasses(teacherUser.id, teacherUser.school.id, 'notAType' as SchoolYearQueryType); + + await expect(func).rejects.toThrow(UnknownQueryTypeLoggableException); + }); + }); }); describe('when accessing as a user with elevated permission', () => { @@ -498,12 +632,14 @@ describe('GroupUc', () => { teacherNames: [teacherUser.lastName], schoolYear: schoolYear.name, isUpgradable: false, + studentCount: 2, }, { id: group.id, name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, { id: groupWithSystem.id, @@ -511,6 +647,7 @@ describe('GroupUc', () => { type: ClassRootType.GROUP, externalSourceName: system.displayName, teacherNames: [teacherUser.lastName], + studentCount: 1, }, ], total: 3, @@ -527,6 +664,7 @@ describe('GroupUc', () => { adminUser.school.id, undefined, undefined, + undefined, 'externalSourceName', SortOrder.desc ); @@ -541,6 +679,7 @@ describe('GroupUc', () => { teacherNames: [teacherUser.lastName], schoolYear: schoolYear.name, isUpgradable: false, + studentCount: 2, }, { id: groupWithSystem.id, @@ -548,12 +687,14 @@ describe('GroupUc', () => { type: ClassRootType.GROUP, externalSourceName: system.displayName, teacherNames: [teacherUser.lastName], + studentCount: 1, }, { id: group.id, name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, ], total: 3, @@ -568,6 +709,7 @@ describe('GroupUc', () => { const result: Page = await uc.findAllClasses( adminUser.id, adminUser.school.id, + undefined, 1, 1, 'name', @@ -581,6 +723,7 @@ describe('GroupUc', () => { name: group.name, type: ClassRootType.GROUP, teacherNames: [teacherUser.lastName], + studentCount: 0, }, ], total: 3, diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index f40750fc852..f7399fa2fc9 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -13,6 +13,8 @@ import { GroupService } from '../service'; import { SortHelper } from '../util'; import { ClassInfoDto, ResolvedGroupDto, ResolvedGroupUser } from './dto'; import { GroupUcMapper } from './mapper/group-uc.mapper'; +import { SchoolYearQueryType } from '../controller/dto/interface'; +import { UnknownQueryTypeLoggableException } from '../loggable'; @Injectable() export class GroupUc { @@ -30,6 +32,7 @@ export class GroupUc { public async findAllClasses( userId: EntityId, schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType, skip = 0, limit?: number, sortBy: keyof ClassInfoDto = 'name', @@ -51,9 +54,9 @@ export class GroupUc { let combinedClassInfo: ClassInfoDto[]; if (canSeeFullList) { - combinedClassInfo = await this.findCombinedClassListForSchool(schoolId); + combinedClassInfo = await this.findCombinedClassListForSchool(schoolId, schoolYearQueryType); } else { - combinedClassInfo = await this.findCombinedClassListForUser(userId); + combinedClassInfo = await this.findCombinedClassListForUser(userId, schoolYearQueryType); } combinedClassInfo.sort((a: ClassInfoDto, b: ClassInfoDto): number => @@ -67,61 +70,142 @@ export class GroupUc { return page; } - private async findCombinedClassListForSchool(schoolId: EntityId): Promise { - const [classInfosFromClasses, classInfosFromGroups] = await Promise.all([ - await this.findClassesForSchool(schoolId), - await this.findGroupsOfTypeClassForSchool(schoolId), - ]); + private async findCombinedClassListForSchool( + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + let classInfosFromGroups: ClassInfoDto[] = []; + + const classInfosFromClasses = await this.findClassesForSchool(schoolId, schoolYearQueryType); + + if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + classInfosFromGroups = await this.findGroupsOfTypeClassForSchool(schoolId); + } const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; return combinedClassInfo; } - private async findCombinedClassListForUser(userId: EntityId): Promise { - const [classInfosFromClasses, classInfosFromGroups] = await Promise.all([ - await this.findClassesForUser(userId), - await this.findGroupsOfTypeClassForUser(userId), - ]); + private async findCombinedClassListForUser( + userId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + let classInfosFromGroups: ClassInfoDto[] = []; + + const classInfosFromClasses = await this.findClassesForUser(userId, schoolYearQueryType); + + if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + classInfosFromGroups = await this.findGroupsOfTypeClassForUser(userId); + } const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; return combinedClassInfo; } - private async findClassesForSchool(schoolId: EntityId): Promise { + private async findClassesForSchool( + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { const classes: Class[] = await this.classService.findClassesForSchool(schoolId); - const classInfosFromClasses: ClassInfoDto[] = await Promise.all( - classes.map((clazz) => this.getClassInfoFromClass(clazz)) - ); + const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); return classInfosFromClasses; } - private async findClassesForUser(userId: EntityId): Promise { + private async findClassesForUser( + userId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { const classes: Class[] = await this.classService.findAllByUserId(userId); - const classInfosFromClasses: ClassInfoDto[] = await Promise.all( - classes.map((clazz) => this.getClassInfoFromClass(clazz)) + const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); + + return classInfosFromClasses; + } + + private async getClassInfosFromClasses( + classes: Class[], + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + const currentYear: SchoolYearEntity = await this.schoolYearService.getCurrentSchoolYear(); + + const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await this.addSchoolYearsToClasses( + classes + ); + + const filteredClassesForSchoolYear = classesWithSchoolYear.filter((classWithSchoolYear) => + this.isClassOfQueryType(currentYear, classWithSchoolYear.schoolYear, schoolYearQueryType) ); + const classInfosFromClasses = await this.mapClassInfosFromClasses(filteredClassesForSchoolYear); + return classInfosFromClasses; } - private async getClassInfoFromClass(clazz: Class): Promise { - const teachers: UserDO[] = await Promise.all( - clazz.teacherIds.map((teacherId: EntityId) => this.userService.findById(teacherId)) + private async addSchoolYearsToClasses(classes: Class[]): Promise<{ clazz: Class; schoolYear?: SchoolYearEntity }[]> { + const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await Promise.all( + classes.map(async (clazz) => { + let schoolYear: SchoolYearEntity | undefined; + if (clazz.year) { + schoolYear = await this.schoolYearService.findById(clazz.year); + } + + return { + clazz, + schoolYear, + }; + }) ); + return classesWithSchoolYear; + } - let schoolYear: SchoolYearEntity | undefined; - if (clazz.year) { - schoolYear = await this.schoolYearService.findById(clazz.year); + private isClassOfQueryType( + currentYear: SchoolYearEntity, + schoolYear?: SchoolYearEntity, + schoolYearQueryType?: SchoolYearQueryType + ): boolean { + if (schoolYearQueryType === undefined) { + return true; } - const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto(clazz, teachers, schoolYear); + if (schoolYear === undefined) { + return schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR; + } - return mapped; + switch (schoolYearQueryType) { + case SchoolYearQueryType.CURRENT_YEAR: + return schoolYear.startDate === currentYear.startDate; + case SchoolYearQueryType.NEXT_YEAR: + return schoolYear.startDate > currentYear.startDate; + case SchoolYearQueryType.PREVIOUS_YEARS: + return schoolYear.startDate < currentYear.startDate; + default: + throw new UnknownQueryTypeLoggableException(schoolYearQueryType); + } + } + + private async mapClassInfosFromClasses( + filteredClassesForSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] + ): Promise { + const classInfosFromClasses = await Promise.all( + filteredClassesForSchoolYear.map(async (classWithSchoolYear): Promise => { + const teachers: UserDO[] = await Promise.all( + classWithSchoolYear.clazz.teacherIds.map((teacherId: EntityId) => this.userService.findById(teacherId)) + ); + + const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto( + classWithSchoolYear.clazz, + teachers, + classWithSchoolYear.schoolYear + ); + + return mapped; + }) + ); + return classInfosFromClasses; } private async findGroupsOfTypeClassForSchool(schoolId: EntityId): Promise { diff --git a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts index f65e8cca602..52ff160921f 100644 --- a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts +++ b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts @@ -19,6 +19,8 @@ export class GroupUcMapper { teacherNames: resolvedUsers .filter((groupUser: ResolvedGroupUser) => groupUser.role.name === RoleName.TEACHER) .map((groupUser: ResolvedGroupUser) => groupUser.user.lastName), + studentCount: resolvedUsers.filter((groupUser: ResolvedGroupUser) => groupUser.role.name === RoleName.STUDENT) + .length, }); return mapped; @@ -36,6 +38,7 @@ export class GroupUcMapper { teacherNames: teachers.map((user: UserDO) => user.lastName), schoolYear: schoolYear?.name, isUpgradable, + studentCount: clazz.userIds ? clazz.userIds.length : 0, }); return mapped;