diff --git a/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.html b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.html new file mode 100644 index 0000000000..6f5d22364d --- /dev/null +++ b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.html @@ -0,0 +1,3 @@ +

+ {{ participantRelationsCount() }} +

diff --git a/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.spec.ts b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.spec.ts new file mode 100644 index 0000000000..64d2a15b2f --- /dev/null +++ b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DisplayParticipantsCountComponent } from "./display-participants-count.component"; +import { ChildrenService } from "../../children/children.service"; +import { School } from "../model/school"; +import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; + +describe("DisplayParticipantsCountComponent", () => { + let component: DisplayParticipantsCountComponent; + let fixture: ComponentFixture; + + let mockChildrenService: jasmine.SpyObj; + + const childSchoolRelations: ChildSchoolRelation[] = [ + new ChildSchoolRelation("r-1"), + new ChildSchoolRelation("r-2"), + new ChildSchoolRelation("r-3"), + ]; + + beforeEach(async () => { + mockChildrenService = jasmine.createSpyObj(["queryActiveRelationsOf"]); + mockChildrenService.queryActiveRelationsOf.and.resolveTo( + childSchoolRelations, + ); + + await TestBed.configureTestingModule({ + imports: [DisplayParticipantsCountComponent], + providers: [{ provide: ChildrenService, useValue: mockChildrenService }], + }).compileComponents(); + + fixture = TestBed.createComponent(DisplayParticipantsCountComponent); + component = fixture.componentInstance; + component.entity = new School("s-1"); + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should count correct number of active students for school", async () => { + expect(component.participantRelationsCount()).toBeNull(); + await component.ngOnChanges(); + expect(component.participantRelationsCount()).toBeDefined(); + expect(component.participantRelationsCount()).toBe(3); + }); + + it("should handle empty response from ChildrenService", async () => { + mockChildrenService.queryActiveRelationsOf.and.resolveTo([]); + expect(component.participantRelationsCount()).toBeNull(); + await component.ngOnChanges(); + expect(component.participantRelationsCount()).toBeDefined(); + expect(component.participantRelationsCount()).toBe(0); + }); + + it("should handle error response from ChildrenService", async () => { + mockChildrenService.queryActiveRelationsOf.and.rejectWith(new Error()); + expect(component.participantRelationsCount()).toBeNull(); + await component.ngOnChanges(); + expect(component.participantRelationsCount()).toBeNull(); + }); +}); diff --git a/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.ts b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.ts new file mode 100644 index 0000000000..25481090fd --- /dev/null +++ b/src/app/child-dev-project/schools/display-participants-count/display-participants-count.component.ts @@ -0,0 +1,44 @@ +import { Component, OnChanges, signal, WritableSignal } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ChildrenService } from "../../children/children.service"; +import { ViewDirective } from "../../../core/entity/default-datatype/view.directive"; +import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; +import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; +import { LoggingService } from "../../../core/logging/logging.service"; + +@DynamicComponent("DisplayParticipantsCount") +@Component({ + selector: "app-display-participants-count", + standalone: true, + imports: [CommonModule], + templateUrl: "./display-participants-count.component.html", +}) +export class DisplayParticipantsCountComponent + extends ViewDirective + implements OnChanges +{ + participantRelationsCount: WritableSignal = signal(null); + + constructor( + private _childrenService: ChildrenService, + private _loggingService: LoggingService, + ) { + super(); + } + + override async ngOnChanges(): Promise { + super.ngOnChanges(); + + return this._childrenService + .queryActiveRelationsOf("school", this.entity.getId()) + .then((relations: ChildSchoolRelation[]) => { + this.participantRelationsCount.set(relations.length); + }) + .catch((reason) => { + this._loggingService.error( + "Could not calculate participantRelationsCount, error response from ChildrenService." + + reason, + ); + }); + } +} diff --git a/src/app/child-dev-project/schools/schools-components.ts b/src/app/child-dev-project/schools/schools-components.ts index 69357634fb..170780ce10 100644 --- a/src/app/child-dev-project/schools/schools-components.ts +++ b/src/app/child-dev-project/schools/schools-components.ts @@ -29,4 +29,11 @@ export const schoolsComponents: ComponentTuple[] = [ (c) => c.SchoolBlockComponent, ), ], + [ + "DisplayParticipantsCount", + () => + import( + "./display-participants-count/display-participants-count.component" + ).then((c) => c.DisplayParticipantsCountComponent), + ], ]; diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index ea1144d0d1..611643676f 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -309,6 +309,7 @@ export const defaultJsonConfig = { "entity": "School", "columns": [ "name", + { id: "DisplayParticipantsCount", viewComponent: "DisplayParticipantsCount", label: $localize`Children` }, "privateSchool", "language" ], diff --git a/src/app/core/entity/default-datatype/view.directive.ts b/src/app/core/entity/default-datatype/view.directive.ts index cb155918e3..c65852f02c 100644 --- a/src/app/core/entity/default-datatype/view.directive.ts +++ b/src/app/core/entity/default-datatype/view.directive.ts @@ -12,6 +12,12 @@ export abstract class ViewDirective implements OnChanges { /** indicating that the value is not in its original state, so that components can explain this to the user */ isPartiallyAnonymized: boolean; + /** + * Attention: + * When content is loaded async in your child component, you need to manually trigger the change detection + * See: https://angularindepth.com/posts/1054/here-is-what-you-need-to-know-about-dynamic-components-in-angular#ngonchanges + * + */ ngOnChanges() { this.isPartiallyAnonymized = this.entity?.anonymized && diff --git a/src/app/core/entity/model/entity.ts b/src/app/core/entity/model/entity.ts index 50fd05b385..1a05718d53 100644 --- a/src/app/core/entity/model/entity.ts +++ b/src/app/core/entity/model/entity.ts @@ -43,7 +43,7 @@ export type EntityConstructor = (new ( * * Entity classes do not deal with database actions, use {@link EntityMapperService} with its find/save/delete functions. * - * Do not use the Entity class directly. Instead implement your own Entity types, writing classes that extend "Entity". + * Do not use the Entity class directly. Instead, implement your own Entity types, writing classes that extend "Entity". * A How-To Guide on how to implement your own types is available: * - [How to Create a new Entity Type]{@link /additional-documentation/how-to-guides/create-a-new-entity-type.html} */