diff --git a/doc/compodoc_sources/concepts/configuration.md b/doc/compodoc_sources/concepts/configuration.md index eff934904b..7a591536ba 100644 --- a/doc/compodoc_sources/concepts/configuration.md +++ b/doc/compodoc_sources/concepts/configuration.md @@ -265,12 +265,13 @@ The component configuration requires another `"title"`, the `"component"` that s "title": "Education", "components": [ { - "title": "SchoolHistory", + "title": "School History", "component": "PreviousSchools" }, { - "title": "ASER Results", - "component": "Aser" + "title": "Literacy Test Results", + "component": "RelatedEntities", + "config": {} } ] } diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts index e052eefc74..23d06654a3 100644 --- a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts +++ b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts @@ -21,7 +21,6 @@ export class ActivitiesOverviewComponent implements OnInit { entityCtr = RecurringActivity; - property = "linkedGroups"; titleColumn: FormFieldConfig = { id: "title", diff --git a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts index 60a04da918..ac5950ac1a 100644 --- a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts +++ b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts @@ -34,7 +34,7 @@ import { EntitiesTableComponent } from "../../../core/common-components/entities standalone: true, }) export class AttendanceDetailsComponent { - @Input() entity: ActivityAttendance = new ActivityAttendance(); + @Input() entity: ActivityAttendance; @Input() forChild: string; EventNote = EventNote; diff --git a/src/app/child-dev-project/attendance/attendance.service.spec.ts b/src/app/child-dev-project/attendance/attendance.service.spec.ts index 4bfeb1ba7c..75bd6f421e 100644 --- a/src/app/child-dev-project/attendance/attendance.service.spec.ts +++ b/src/app/child-dev-project/attendance/attendance.service.spec.ts @@ -287,7 +287,7 @@ describe("AttendanceService", () => { const childAttendingSchool = new ChildSchoolRelation(); childAttendingSchool.childId = "child attending school"; - const mockQueryRelationsOf = spyOn( + const mockQueryRelations = spyOn( TestBed.inject(ChildrenService), "queryActiveRelationsOf", ).and.resolveTo([childAttendingSchool]); @@ -298,10 +298,7 @@ describe("AttendanceService", () => { const event = await service.createEventForActivity(activity, date); - expect(mockQueryRelationsOf).toHaveBeenCalledWith( - linkedSchool.getId(), - date, - ); + expect(mockQueryRelations).toHaveBeenCalledWith(linkedSchool.getId(), date); expect(event.children).toHaveSize(2); expect(event.children).toContain(directlyAddedChild.getId()); expect(event.children).toContain(childAttendingSchool.childId); diff --git a/src/app/child-dev-project/children/aser/aser-component/aser.component.spec.ts b/src/app/child-dev-project/children/aser/aser-component/aser.component.spec.ts deleted file mode 100644 index 3186c44257..0000000000 --- a/src/app/child-dev-project/children/aser/aser-component/aser.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { AserComponent } from "./aser.component"; -import { ChildrenService } from "../../children.service"; -import { Child } from "../../model/child"; -import { of } from "rxjs"; -import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; - -describe("AserComponent", () => { - let component: AserComponent; - let fixture: ComponentFixture; - - const mockChildrenService = { - getChild: () => { - return of([new Child("22")]); - }, - getAserResultsOfChild: () => { - return of([]); - }, - }; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [AserComponent, MockedTestingModule.withState()], - providers: [{ provide: ChildrenService, useValue: mockChildrenService }], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AserComponent); - component = fixture.componentInstance; - component.entity = new Child("22"); - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/child-dev-project/children/aser/aser-component/aser.component.ts b/src/app/child-dev-project/children/aser/aser-component/aser.component.ts deleted file mode 100644 index 9a3b99c1b6..0000000000 --- a/src/app/child-dev-project/children/aser/aser-component/aser.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { Aser } from "../model/aser"; -import { Child } from "../../model/child"; -import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; -import { EntitiesTableComponent } from "../../../../core/common-components/entities-table/entities-table.component"; - -import { FormFieldConfig } from "../../../../core/common-components/entity-form/FormConfig"; -import { RelatedEntitiesComponent } from "../../../../core/entity-details/related-entities/related-entities.component"; - -@DynamicComponent("Aser") -@Component({ - selector: "app-aser", - templateUrl: - "../../../../core/entity-details/related-entities/related-entities.component.html", - standalone: true, - imports: [EntitiesTableComponent], -}) -export class AserComponent extends RelatedEntitiesComponent { - @Input() entity: Child; - property = "child"; - entityCtr = Aser; - - override _columns: FormFieldConfig[] = [ - { id: "date", visibleFrom: "xs" }, - { id: "math", visibleFrom: "xs" }, - { id: "english", visibleFrom: "xs" }, - { id: "hindi", visibleFrom: "md" }, - { id: "bengali", visibleFrom: "md" }, - { id: "remarks", visibleFrom: "md" }, - ]; - - override async initData() { - super - .initData() - .then(() => - this.data.sort( - (a, b) => - (b.date ? b.date.valueOf() : 0) - (a.date ? a.date.valueOf() : 0), - ), - ); - } -} diff --git a/src/app/child-dev-project/children/children-components.ts b/src/app/child-dev-project/children/children-components.ts index c39c2287e0..ae1382e88f 100644 --- a/src/app/child-dev-project/children/children-components.ts +++ b/src/app/child-dev-project/children/children-components.ts @@ -23,13 +23,6 @@ export const childrenComponents: ComponentTuple[] = [ "./children-list/recent-attendance-blocks/recent-attendance-blocks.component" ).then((c) => c.RecentAttendanceBlocksComponent), ], - [ - "Aser", - () => - import("./aser/aser-component/aser.component").then( - (c) => c.AserComponent, - ), - ], [ "ChildBlock", () => @@ -37,25 +30,4 @@ export const childrenComponents: ComponentTuple[] = [ (c) => c.ChildBlockComponent, ), ], - [ - "ChildrenBmiDashboard", - () => - import( - "./health-checkup/children-bmi-dashboard/children-bmi-dashboard.component" - ).then((c) => c.ChildrenBmiDashboardComponent), - ], - [ - "BmiBlock", - () => - import("./health-checkup/bmi-block/bmi-block.component").then( - (c) => c.BmiBlockComponent, - ), - ], - [ - "HealthCheckup", - () => - import( - "./health-checkup/health-checkup-component/health-checkup.component" - ).then((c) => c.HealthCheckupComponent), - ], ]; diff --git a/src/app/child-dev-project/children/children.service.ts b/src/app/child-dev-project/children/children.service.ts index 3ba042cc8b..08b9c9a445 100644 --- a/src/app/child-dev-project/children/children.service.ts +++ b/src/app/child-dev-project/children/children.service.ts @@ -3,7 +3,6 @@ import { Child } from "./model/child"; import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service"; import { Note } from "../notes/model/note"; import { ChildSchoolRelation } from "./model/childSchoolRelation"; -import { HealthCheck } from "./health-checkup/model/health-check"; import moment, { Moment } from "moment"; import { DatabaseIndexingService } from "../../core/entity/database-indexing/database-indexing.service"; import { Entity } from "../../core/entity/model/entity"; @@ -212,7 +211,7 @@ export class ChildrenService { if (!Array.isArray(doc.children) || !doc.date) return; if (doc.date.length === 10) { emit(doc.date); - } else { + } else { var d = new Date(doc.date || null); emit(d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0")); } @@ -240,7 +239,7 @@ export class ChildrenService { var dString; if (doc.date && doc.date.length === 10) { dString = doc.date; - } else { + } else { var d = new Date(doc.date || null); dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } @@ -263,15 +262,4 @@ export class ChildrenService { }`, }; } - - /** - * - * @param childId should be set in the specific components and is passed by the URL as a parameter - * This function should be considered refactored and should use a index, once they're made generic - */ - getHealthChecksOfChild(childId: string): Promise { - return this.entityMapper - .loadType(HealthCheck) - .then((res) => res.filter((h) => h.child === childId)); - } } diff --git a/src/app/child-dev-project/children/educational-material/model/educational-material.ts b/src/app/child-dev-project/children/educational-material/model/educational-material.ts index adf6490993..30e191d097 100644 --- a/src/app/child-dev-project/children/educational-material/model/educational-material.ts +++ b/src/app/child-dev-project/children/educational-material/model/educational-material.ts @@ -19,7 +19,9 @@ import { Entity } from "../../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../../core/entity/database-field.decorator"; import { ConfigurableEnumValue } from "../../../../core/basic-datatypes/configurable-enum/configurable-enum.interface"; +import { EntityDatatype } from "../../../../core/basic-datatypes/entity/entity.datatype"; import { Child } from "../../model/child"; +import { PLACEHOLDERS } from "../../../../core/entity/schema/entity-schema-field"; @DatabaseEntity("EducationalMaterial") export class EducationalMaterial extends Entity { @@ -28,7 +30,7 @@ export class EducationalMaterial extends Entity { } @DatabaseField({ - dataType: "entity", + dataType: EntityDatatype.dataType, additional: Child.ENTITY_TYPE, entityReferenceRole: "composite", }) @@ -36,6 +38,7 @@ export class EducationalMaterial extends Entity { @DatabaseField({ label: $localize`:Date on which the material has been borrowed:Date`, + defaultValue: PLACEHOLDERS.NOW, }) date: Date; diff --git a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.scss b/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.scss deleted file mode 100644 index 347006fa35..0000000000 --- a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -.bmi-block { - border-radius: 4px; - padding: 5px; -} diff --git a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.spec.ts b/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.spec.ts deleted file mode 100644 index 3ceae2c8f2..0000000000 --- a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from "@angular/core/testing"; -import { HealthCheck } from "../model/health-check"; -import { ChildrenService } from "../../children.service"; -import { Child } from "../../model/child"; - -import { BmiBlockComponent } from "./bmi-block.component"; - -describe("BmiBlockComponent", () => { - let component: BmiBlockComponent; - let fixture: ComponentFixture; - const mockChildrenService: jasmine.SpyObj = - jasmine.createSpyObj("mockChildrenService", ["getHealthChecksOfChild"]); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [BmiBlockComponent], - providers: [{ provide: ChildrenService, useValue: mockChildrenService }], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(BmiBlockComponent); - component = fixture.componentInstance; - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should load the BMI data for the child", fakeAsync(() => { - const testChild = new Child("testID"); - const healthCheck1 = HealthCheck.create({ - date: new Date("2020-10-30"), - height: 1.3, - weight: 60, - }); - const healthCheck2 = HealthCheck.create({ - date: new Date("2020-11-30"), - height: 1.5, - weight: 77, - }); - const healthCheck3 = HealthCheck.create({ - date: new Date("2020-09-30"), - height: 1.15, - weight: 50, - }); - mockChildrenService.getHealthChecksOfChild.and.resolveTo([ - healthCheck1, - healthCheck2, - healthCheck3, - ]); - component.entity = testChild; - fixture.detectChanges(); - - expect(mockChildrenService.getHealthChecksOfChild).toHaveBeenCalledWith( - testChild.getId(), - ); - tick(); - expect(component.currentHealthCheck).toEqual(healthCheck2); - })); -}); diff --git a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.ts b/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.ts deleted file mode 100644 index 65f906ea60..0000000000 --- a/src/app/child-dev-project/children/health-checkup/bmi-block/bmi-block.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, Input, OnInit } from "@angular/core"; -import { HealthCheck } from "../model/health-check"; -import { ChildrenService } from "../../children.service"; -import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; -import { Child } from "../../model/child"; - -@DynamicComponent("BmiBlock") -@Component({ - selector: "app-bmi-block", - template: ` - {{ currentHealthCheck?.bmi.toFixed(2) }} - `, - styleUrls: ["./bmi-block.component.scss"], - standalone: true, -}) -export class BmiBlockComponent implements OnInit { - @Input() entity: Child; - currentHealthCheck: HealthCheck; - - constructor(private childrenService: ChildrenService) {} - - ngOnInit() { - this.childrenService - .getHealthChecksOfChild(this.entity.getId()) - .then((results) => { - if (results.length > 0) { - this.currentHealthCheck = results.reduce((prev, cur) => - cur.date > prev.date ? cur : prev, - ); - } - }); - } -} diff --git a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.html b/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.html deleted file mode 100644 index 19affffdd9..0000000000 --- a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.html +++ /dev/null @@ -1,63 +0,0 @@ - - -
- - - - - - - - - - - - - - - -
- - - BMI: {{ row.bmi | number: "1.0-2" }} -
-
-
- no BMI data recorded -
- - -
-
diff --git a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.scss b/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.scss deleted file mode 100644 index c9b48bcc9d..0000000000 --- a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.scss +++ /dev/null @@ -1 +0,0 @@ -@use "../../../../core/dashboard/dashboard-widget-base"; diff --git a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.spec.ts b/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.spec.ts deleted file mode 100644 index ae0455a11b..0000000000 --- a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from "@angular/core/testing"; -import { ChildrenBmiDashboardComponent } from "./children-bmi-dashboard.component"; -import { HealthCheck } from "../model/health-check"; -import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; -import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service"; - -describe("ChildrenBmiDashboardComponent", () => { - let component: ChildrenBmiDashboardComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ChildrenBmiDashboardComponent, MockedTestingModule.withState()], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ChildrenBmiDashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should load the BMI data for the children, but only display the unhealthy one", fakeAsync(() => { - const healthCheck1 = HealthCheck.create({ - child: "testID", - date: new Date("2020-10-30"), - height: 130, - weight: 60, - }); - const healthCheck2 = HealthCheck.create({ - child: "testID", - date: new Date("2020-11-30"), - height: 150, - weight: 15, - }); - const healthCheck3 = HealthCheck.create({ - child: "testID2", - date: new Date("2020-09-30"), - height: 115, - weight: 30, - }); - const loadTypeSpy = spyOn(TestBed.inject(EntityMapperService), "loadType"); - loadTypeSpy.and.resolveTo([healthCheck1, healthCheck2, healthCheck3]); - - component.ngOnInit(); - - expect(loadTypeSpy).toHaveBeenCalledWith(HealthCheck); - tick(); - expect(component.bmiDataSource.data).toEqual([ - { childId: "testID", bmi: healthCheck2.bmi }, - ]); - })); -}); diff --git a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.ts b/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.ts deleted file mode 100644 index 1b8ba92980..0000000000 --- a/src/app/child-dev-project/children/health-checkup/children-bmi-dashboard/children-bmi-dashboard.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core"; -import { MatTableDataSource, MatTableModule } from "@angular/material/table"; -import { MatPaginator, MatPaginatorModule } from "@angular/material/paginator"; -import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service"; -import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; -import { WarningLevel } from "../../../warning-level"; -import { HealthCheck } from "../model/health-check"; -import { groupBy } from "../../../../utils/utils"; -import { Child } from "../../model/child"; -import { DecimalPipe, NgIf } from "@angular/common"; -import { DisplayEntityComponent } from "../../../../core/basic-datatypes/entity/display-entity/display-entity.component"; -import { DashboardWidgetComponent } from "../../../../core/dashboard/dashboard-widget/dashboard-widget.component"; -import { WidgetContentComponent } from "../../../../core/dashboard/dashboard-widget/widget-content/widget-content.component"; -import { DashboardWidget } from "../../../../core/dashboard/dashboard-widget/dashboard-widget"; - -interface BmiRow { - childId: string; - bmi: number; -} - -@DynamicComponent("ChildrenBmiDashboard") -@Component({ - selector: "app-children-bmi-dashboard", - templateUrl: "./children-bmi-dashboard.component.html", - styleUrls: ["./children-bmi-dashboard.component.scss"], - imports: [ - NgIf, - MatTableModule, - DecimalPipe, - MatPaginatorModule, - DisplayEntityComponent, - DashboardWidgetComponent, - WidgetContentComponent, - ], - standalone: true, -}) -export class ChildrenBmiDashboardComponent - extends DashboardWidget - implements OnInit, AfterViewInit -{ - static getRequiredEntities() { - return HealthCheck.ENTITY_TYPE; - } - - bmiDataSource = new MatTableDataSource(); - isLoading = true; - entityLabelPlural: string = Child.labelPlural; - @ViewChild("paginator") paginator: MatPaginator; - - constructor(private entityMapper: EntityMapperService) { - super(); - } - - ngOnInit() { - return this.loadBMIData(); - } - - ngAfterViewInit() { - this.bmiDataSource.paginator = this.paginator; - } - - async loadBMIData() { - // Maybe replace this by a smart index function - const healthChecks = await this.entityMapper.loadType(HealthCheck); - const BMIs: BmiRow[] = []; - groupBy(healthChecks, "child").forEach(([childId, checks]) => { - const latest = checks.reduce((prev, cur) => - cur.date > prev.date ? cur : prev, - ); - if (latest && latest.getWarningLevel() === WarningLevel.URGENT) { - BMIs.push({ childId: childId, bmi: latest?.bmi }); - } - }); - this.bmiDataSource.data = BMIs; - this.isLoading = false; - } -} diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.spec.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.spec.ts deleted file mode 100644 index eb4ebb3916..0000000000 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; - -import { HealthCheckupComponent } from "./health-checkup.component"; -import { Child } from "../../model/child"; -import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; - -describe("HealthCheckupComponent", () => { - let component: HealthCheckupComponent; - let fixture: ComponentFixture; - const child = new Child(); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HealthCheckupComponent, MockedTestingModule.withState()], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(HealthCheckupComponent); - component = fixture.componentInstance; - component.entity = child; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts deleted file mode 100644 index 914f565c3b..0000000000 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, Input, OnInit } from "@angular/core"; -import { HealthCheck } from "../model/health-check"; -import { Child } from "../../model/child"; -import { FormFieldConfig } from "../../../../core/common-components/entity-form/FormConfig"; -import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; -import { EntitiesTableComponent } from "../../../../core/common-components/entities-table/entities-table.component"; -import { RelatedEntitiesComponent } from "../../../../core/entity-details/related-entities/related-entities.component"; - -@DynamicComponent("HealthCheckup") -@Component({ - selector: "app-health-checkup", - templateUrl: - "../../../../core/entity-details/related-entities/related-entities.component.html", - imports: [EntitiesTableComponent], - standalone: true, -}) -export class HealthCheckupComponent - extends RelatedEntitiesComponent - implements OnInit -{ - @Input() entity: Child; - property = "child"; - entityCtr = HealthCheck; - - /** - * Column Description - * The Date-Column needs to be transformed to apply the MathFormCheck in the SubentityRecordComponent - * BMI is rounded to 2 decimal digits - */ - override _columns: FormFieldConfig[] = [ - { id: "date" }, - { id: "height" }, - { id: "weight" }, - { - id: "bmi", - label: $localize`:Table header, Short for Body Mass Index:BMI`, - viewComponent: "ReadonlyFunction", - description: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, - additional: (entity: HealthCheck) => this.getBMI(entity), - }, - ]; - - private getBMI(healthCheck: HealthCheck): string { - const bmi = healthCheck.bmi; - if (Number.isNaN(bmi)) { - return "-"; - } else { - return bmi.toFixed(2); - } - } - - override createNewRecordFactory() { - return () => { - const newHC = new HealthCheck(); - - newHC.date = new Date(); - newHC.child = this.entity.getId(); - - return newHC; - }; - } - - /** - * implements the health check loading from the children service and is called in the onInit() - */ - override async initData() { - super - .initData() - .then(() => - this.data.sort( - (a, b) => - (b.date ? b.date.valueOf() : 0) - (a.date ? a.date.valueOf() : 0), - ), - ); - } -} diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.stories.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.stories.ts deleted file mode 100644 index 46e3122d63..0000000000 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.stories.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { applicationConfig, Meta, StoryFn } from "@storybook/angular"; -import { HealthCheckupComponent } from "./health-checkup.component"; -import { ChildrenService } from "../../children.service"; -import { HealthCheck } from "../model/health-check"; -import moment from "moment"; -import { Child } from "../../model/child"; -import { importProvidersFrom } from "@angular/core"; -import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; - -const hc1 = new HealthCheck(); -hc1.date = new Date(); -hc1.height = 200; -hc1.weight = 70; -const hc2 = new HealthCheck(); -hc2.date = moment().subtract(1, "year").toDate(); -hc2.height = 178; -hc2.weight = 65; -const hc3 = new HealthCheck(); -hc3.date = moment().subtract(2, "years").toDate(); -hc3.height = 175; -hc3.weight = 80; - -export default { - title: "Features/Health Checkup", - component: HealthCheckupComponent, - decorators: [ - applicationConfig({ - providers: [ - importProvidersFrom(StorybookBaseModule.withData()), - { - provide: ChildrenService, - useValue: { - getHealthChecksOfChild: () => Promise.resolve([hc1, hc2, hc3]), - }, - }, - ], - }), - ], -} as Meta; - -const Template: StoryFn = ( - args: HealthCheckupComponent, -) => ({ - component: HealthCheckupComponent, - props: args, -}); - -export const Primary = Template.bind({}); -Primary.args = { - entity: new Child(), -}; diff --git a/src/app/child-dev-project/children/health-checkup/model/health-check.ts b/src/app/child-dev-project/children/health-checkup/model/health-check.ts index b8ad26bcca..31cfad7dc2 100644 --- a/src/app/child-dev-project/children/health-checkup/model/health-check.ts +++ b/src/app/child-dev-project/children/health-checkup/model/health-check.ts @@ -20,6 +20,7 @@ import { DatabaseEntity } from "../../../../core/entity/database-entity.decorato import { DatabaseField } from "../../../../core/entity/database-field.decorator"; import { WarningLevel } from "../../../warning-level"; import { Child } from "../../model/child"; +import { PLACEHOLDERS } from "../../../../core/entity/schema/entity-schema-field"; /** * Model Class for the Health Checks that are taken for a Child. @@ -44,6 +45,7 @@ export class HealthCheck extends Entity { @DatabaseField({ label: $localize`:Label for date of a health check:Date`, anonymize: "retain-anonymized", + defaultValue: PLACEHOLDERS.NOW, }) date: Date; @@ -63,8 +65,12 @@ export class HealthCheck extends Entity { }) weight: number; + /** + * dynamically calculated BMI value based on the height and weight, rounded to 2 decimal digits + */ get bmi(): number { - return this.weight / ((this.height / 100) * (this.height / 100)); + const bmi = this.weight / ((this.height / 100) * (this.height / 100)); + return Math.round(bmi * 100) / 100; } getWarningLevel(): WarningLevel { diff --git a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts index 3aeeb4c761..007f39907c 100644 --- a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts +++ b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts @@ -1,44 +1,31 @@ import { NotesRelatedToEntityComponent } from "./notes-related-to-entity.component"; -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from "@angular/core/testing"; -import { ChildrenService } from "../../children/children.service"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { Note } from "../model/note"; import { Child } from "../../children/model/child"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { Entity } from "../../../core/entity/model/entity"; import { School } from "../../schools/model/school"; -import { User } from "../../../core/user/user"; -import moment from "moment"; -import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../core/entity/database-field.decorator"; +import { User } from "../../../core/user/user"; +import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; describe("NotesRelatedToEntityComponent", () => { let component: NotesRelatedToEntityComponent; let fixture: ComponentFixture; - let mockChildrenService: jasmine.SpyObj; - beforeEach(waitForAsync(() => { - mockChildrenService = jasmine.createSpyObj(["getNotesRelatedTo"]); - mockChildrenService.getNotesRelatedTo.and.resolveTo([]); TestBed.configureTestingModule({ imports: [NotesRelatedToEntityComponent, MockedTestingModule.withState()], - providers: [{ provide: ChildrenService, useValue: mockChildrenService }], }).compileComponents(); })); - beforeEach(async () => { + beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(NotesRelatedToEntityComponent); component = fixture.componentInstance; component.entity = new Child("1"); fixture.detectChanges(); - }); + })); it("should create", () => { expect(component).toBeTruthy(); @@ -56,32 +43,40 @@ describe("NotesRelatedToEntityComponent", () => { expect(note.getColorForId).toHaveBeenCalledWith(entity.getId()); }); - it("should create a new note and fill it with the appropriate initial value", () => { + it("should create a new note and fill it with the appropriate initial value", async () => { let entity: Entity = new Child(); component.entity = entity; - component.ngOnInit(); - let note = component.generateNewRecordFactory()(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + let note = component.createNewRecordFactory()(); expect(note.children).toEqual([entity.getId()]); entity = new School(); component.entity = entity; - component.ngOnInit(); - note = component.generateNewRecordFactory()(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + note = component.createNewRecordFactory()(); expect(note.schools).toEqual([entity.getId()]); entity = new User(); component.entity = entity; - component.ngOnInit(); - note = component.generateNewRecordFactory()(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + note = component.createNewRecordFactory()(); expect(note.relatedEntities).toEqual([entity.getId()]); entity = new ChildSchoolRelation(); entity["childId"] = `${Child.ENTITY_TYPE}:someChild`; entity["schoolId"] = `${Child.ENTITY_TYPE}:someSchool`; component.entity = entity; - component.ngOnInit(); - note = component.generateNewRecordFactory()(); - expect(note.relatedEntities).toContain(entity.getId()); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + note = component.createNewRecordFactory()(); + expect(note.relatedEntities).toEqual([entity.getId()]); expect(note.children).toEqual([`${Child.ENTITY_TYPE}:someChild`]); expect(note.schools).toEqual([`${Child.ENTITY_TYPE}:someSchool`]); }); @@ -110,6 +105,7 @@ describe("NotesRelatedToEntityComponent", () => { ]; customEntity.childrenLink = `${Child.ENTITY_TYPE}:child-without-prefix`; + const schemaBefore = Note.schema.get("relatedEntities").additional; Note.schema.get("relatedEntities").additional = [ Child.ENTITY_TYPE, EntityWithRelations.ENTITY_TYPE, @@ -117,7 +113,7 @@ describe("NotesRelatedToEntityComponent", () => { component.entity = customEntity; component.ngOnInit(); - const newNote = component.generateNewRecordFactory()(); + const newNote = component.createNewRecordFactory()(); expect(newNote.relatedEntities).toContain(customEntity.getId()); expect(newNote.relatedEntities).toContain(customEntity.links[0]); @@ -125,24 +121,7 @@ describe("NotesRelatedToEntityComponent", () => { expect(newNote.relatedEntities).toContain( Entity.createPrefixedId(Child.ENTITY_TYPE, customEntity.childrenLink), ); - }); - - it("should sort notes by date", fakeAsync(() => { - // No date should come first - const n1 = new Note(); - const n2 = new Note(); - n2.date = moment().subtract(1, "day").toDate(); - const n3 = new Note(); - n3.date = moment().subtract(2, "days").toDate(); - mockChildrenService.getNotesRelatedTo.and.resolveTo([n3, n2, n1]); - component.entity = new Child(); - component.ngOnInit(); - tick(); - - expect(mockChildrenService.getNotesRelatedTo).toHaveBeenCalledWith( - component.entity.getId(), - ); - expect(component.data).toEqual([n1, n2, n3]); - })); + Note.schema.get("relatedEntities").additional = schemaBefore; + }); }); diff --git a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts index 85dfa400d1..854d2fee0e 100644 --- a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts +++ b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts @@ -2,13 +2,11 @@ import { Component } from "@angular/core"; import { Note } from "../model/note"; import { NoteDetailsComponent } from "../note-details/note-details.component"; import { ChildrenService } from "../../children/children.service"; -import moment from "moment"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; import { Entity } from "../../../core/entity/model/entity"; import { FilterService } from "../../../core/filter/filter.service"; import { Child } from "../../children/model/child"; -import { School } from "../../schools/model/school"; import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; import { EntityDatatype } from "../../../core/basic-datatypes/entity/entity.datatype"; import { EntityArrayDatatype } from "../../../core/basic-datatypes/entity-array/entity-array.datatype"; @@ -46,17 +44,17 @@ export class NotesRelatedToEntityComponent extends RelatedEntitiesComponent note?.getColor(); - newRecordFactory = this.generateNewRecordFactory(); + newRecordFactory = this.createNewRecordFactory(); constructor( private childrenService: ChildrenService, private formDialog: FormDialogService, - private filterService: FilterService, entityMapper: EntityMapperService, entities: EntityRegistry, - screenWidthOberserver: ScreenWidthObserver, + screenWidthObserver: ScreenWidthObserver, + filterService: FilterService, ) { - super(entityMapper, entities, screenWidthOberserver); + super(entityMapper, entities, screenWidthObserver, filterService); } override ngOnInit() { @@ -67,31 +65,15 @@ export class NotesRelatedToEntityComponent extends RelatedEntitiesComponent { - notes.sort((a, b) => { - if (!a.date && b.date) { - // note without date should be first - return -1; - } - return moment(b.date).valueOf() - moment(a.date).valueOf(); - }); - return notes; - }); + override getData() { + return this.childrenService.getNotesRelatedTo(this.entity.getId()); } - generateNewRecordFactory() { + override createNewRecordFactory() { return () => { - const newNote = new Note(Date.now().toString()); - + const newNote = super.createNewRecordFactory()(); //TODO: generalize this code - possibly by only using relatedEntities to link other records here? see #1501 - if (this.entity.getType() === Child.ENTITY_TYPE) { - newNote.addChild(this.entity as Child); - } else if (this.entity.getType() === School.ENTITY_TYPE) { - newNote.addSchool(this.entity as School); - } else if (this.entity.getType() === ChildSchoolRelation.ENTITY_TYPE) { + if (this.entity.getType() === ChildSchoolRelation.ENTITY_TYPE) { newNote.addChild((this.entity as ChildSchoolRelation).childId); newNote.addSchool((this.entity as ChildSchoolRelation).schoolId); } @@ -101,8 +83,6 @@ export class NotesRelatedToEntityComponent extends RelatedEntitiesComponent { let mockChildrenService: jasmine.SpyObj; - const testChild = new Child("22"); - const inactive = new ChildSchoolRelation("r2"); - inactive.end = moment().subtract("1", "week").toDate(); - beforeEach(waitForAsync(() => { mockChildrenService = jasmine.createSpyObj(["queryRelations"]); mockChildrenService.queryRelations.and.resolveTo([ @@ -28,49 +30,51 @@ describe("ChildSchoolOverviewComponent", () => { imports: [ChildSchoolOverviewComponent, MockedTestingModule.withState()], providers: [{ provide: ChildrenService, useValue: mockChildrenService }], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ChildSchoolOverviewComponent); component = fixture.componentInstance; - component.entity = testChild; - fixture.detectChanges(); - }); + })); it("should create", () => { expect(component).toBeTruthy(); }); - it("it calls children service with id from passed child", async () => { - await component.ngOnInit(); + it("it calls children service with id from passed child", fakeAsync(() => { + component.entity = new Child(); + + fixture.detectChanges(); + tick(); + expect(mockChildrenService.queryRelations).toHaveBeenCalledWith( - testChild.getId(), + component.entity.getId(), ); - }); + })); - it("it detects mode and uses correct index to load data ", async () => { + it("it detects mode and uses correct index to load data ", fakeAsync(() => { const testSchool = new School(); component.entity = testSchool; - await component.ngOnInit(); + fixture.detectChanges(); + tick(); expect(component.mode).toBe("school"); expect(mockChildrenService.queryRelations).toHaveBeenCalledWith( testSchool.getId(), ); - }); + })); - it("should create a relation with the child ID", async () => { + it("should create a relation with the child ID", fakeAsync(() => { + const child = new Child(); const existingRelation = new ChildSchoolRelation(); + existingRelation.childId = child.getId(); existingRelation.start = moment().subtract(1, "year").toDate(); existingRelation.end = moment().subtract(1, "week").toDate(); mockChildrenService.queryRelations.and.resolveTo([existingRelation]); - const child = new Child(); component.entity = child; - await component.ngOnInit(); + fixture.detectChanges(); + tick(); - const newRelation = component.generateNewRecordFactory()(); + const newRelation = component.createNewRecordFactory()(); expect(newRelation.childId).toEqual(child.getId()); expect( @@ -78,15 +82,34 @@ describe("ChildSchoolOverviewComponent", () => { .add(1, "day") .isSame(newRelation.start, "day"), ).toBeTrue(); - }); + })); - it("should create a relation with the school ID", () => { + it("should create a relation with the school ID", fakeAsync(() => { component.entity = new School("testID"); - component.ngOnInit(); + fixture.detectChanges(); + tick(); - const newRelation = component.generateNewRecordFactory()(); + const newRelation = component.createNewRecordFactory()(); expect(newRelation).toBeInstanceOf(ChildSchoolRelation); expect(newRelation.schoolId).toBe("School:testID"); - }); + })); + + it("should show archived school in 'child' mode", fakeAsync(() => { + component.entity = new Child(); + + fixture.detectChanges(); + tick(); + + expect(component.showInactive).toBeTrue(); + })); + + it("should not show archived children in 'school' mode", fakeAsync(() => { + component.entity = new School(); + + fixture.detectChanges(); + tick(); + + expect(component.showInactive).toBeFalse(); + })); }); diff --git a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts index 380dfb680e..630aec3b10 100644 --- a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts +++ b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts @@ -1,10 +1,7 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; -import { Child } from "../../children/model/child"; -import { School } from "../model/school"; import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; import { ChildrenService } from "../../children/children.service"; -import { Entity } from "../../../core/entity/model/entity"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { FormsModule } from "@angular/forms"; @@ -16,6 +13,7 @@ import { EntitiesTableComponent } from "../../../core/common-components/entities import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; +import { FilterService } from "../../../core/filter/filter.service"; // TODO: once schema-generated indices are available (#262), remove this component and use its generic super class directly @DynamicComponent("ChildSchoolOverview") @@ -44,7 +42,6 @@ export class ChildSchoolOverviewComponent implements OnInit { mode: "child" | "school" = "child"; - @Input() showInactive = this.mode === "child"; entityCtr = ChildSchoolRelation; constructor( @@ -52,8 +49,9 @@ export class ChildSchoolOverviewComponent entityMapper: EntityMapperService, entityRegistry: EntityRegistry, screenWidthObserver: ScreenWidthObserver, + filterService: FilterService, ) { - super(entityMapper, entityRegistry, screenWidthObserver); + super(entityMapper, entityRegistry, screenWidthObserver, filterService); this.columns = [ { id: "childId" }, // schoolId/childId replaced dynamically during init @@ -64,23 +62,15 @@ export class ChildSchoolOverviewComponent ]; } - async ngOnInit() { - this.mode = this.inferMode(this.entity); + override async ngOnInit(): Promise { + this.mode = this.entity.getType().toLowerCase() as any; this.showInactive = this.mode === "child"; this.switchRelatedEntityColumnForMode(); - await super.ngOnInit(); } - private inferMode(entity: Entity): "child" | "school" { - switch (entity?.getConstructor()?.ENTITY_TYPE) { - case Child.ENTITY_TYPE: - this.property = "childId"; - return "child"; - case School.ENTITY_TYPE: - this.property = "schoolId"; - return "school"; - } + override getData() { + return this.childrenService.queryRelations(this.entity.getId(false)); } private switchRelatedEntityColumnForMode() { @@ -92,8 +82,4 @@ export class ChildSchoolOverviewComponent idColumn.id = this.mode === "child" ? "schoolId" : "childId"; } } - - override async initData() { - this.data = await this.childrenService.queryRelations(this.entity.getId()); - } } diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index e17a7b7711..55977aa5cf 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -155,9 +155,6 @@ export const defaultJsonConfig = { { "component": "BirthdayDashboard" }, - { - "component": "ChildrenBmiDashboard" - }, ] } }, @@ -401,12 +398,6 @@ export const defaultJsonConfig = { "filterByActivityType": "COACHING_CLASS" }, "noSorting": true - }, - { - "viewComponent": "BmiBlock", - "label": $localize`:Column label for BMI of child:BMI`, - "id": "health_BMI", - "noSorting": true } ], "columnGroups": { @@ -455,7 +446,6 @@ export const defaultJsonConfig = { "projectNumber", "name", "center", - "health_BMI", "health_bloodGroup", "health_lastDentalCheckup", "gender", @@ -549,7 +539,37 @@ export const defaultJsonConfig = { }, { "title": $localize`:Title inside a panel:ASER Results`, - "component": "Aser" + "component": "RelatedEntities", + "config": { + "entityType": "Aser", + "property": "child", + "columns": [ + { + "id": "date", + "visibleFrom": "xs" + }, + { + "id": "math", + "visibleFrom": "xs" + }, + { + "id": "english", + "visibleFrom": "xs" + }, + { + "id": "hindi", + "visibleFrom": "md" + }, + { + "id": "bengali", + "visibleFrom": "md" + }, + { + "id": "remarks", + "visibleFrom": "md" + } + ] + } }, { "title": $localize`:Child details section title:Find a suitable new school`, @@ -605,7 +625,22 @@ export const defaultJsonConfig = { }, { "title": $localize`:Title inside a panel:Height & Weight Tracking`, - "component": "HealthCheckup" + "component": "RelatedEntities", + "config": { + "entityType": "HealthCheck", + "property": "child", + "columns": [ + { "id": "date" }, + { "id": "height" }, + { "id": "weight" }, + { + "id": "bmi", + "label": $localize`:Table header, Short for Body Mass Index:BMI`, + "viewComponent": "DisplayText", + "description": $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, + } + ] + } } ] }, diff --git a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts index 5c17e54951..2e9ae3f67b 100644 --- a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts +++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts @@ -45,7 +45,6 @@ describe("RelatedEntitiesWithSummaryComponent", () => { component = fixture.componentInstance; component.entity = child; component.entityType = EducationalMaterial.ENTITY_TYPE; - component.property = "child"; component.summaries = { countProperty: "materialAmount", @@ -63,7 +62,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { it("produces an empty summary when there are no records", () => { component.data = []; - component.updateSummary(); + component.updateSummary(component.data); expect(component.summarySum).toHaveSize(0); expect(component.summaryAvg).toHaveSize(0); }); @@ -72,7 +71,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { ...records: Partial[] ) { component.data = records.map(EducationalMaterial.create); - component.updateSummary(); + component.updateSummary(component.data); } it("produces a singleton summary when there is a single record", () => { @@ -99,7 +98,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { component.data = [{ amount: 1 }, { amount: 5 }] as any[]; delete component.summaries.groupBy; component.summaries.countProperty = "amount"; - component.updateSummary(); + component.updateSummary(component.data); expect(component.summarySum).toEqual(`6`); expect(component.summaryAvg).toEqual(`3`); @@ -178,7 +177,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { ); }); - it("loads all education data associated with a child and updates the summary", async () => { + it("loads all education data associated with a child and updates the summary", fakeAsync(() => { const educationalData = [ { materialType: PENCIL, materialAmount: 1, child: child.getId() }, { materialType: RULER, materialAmount: 2, child: child.getId() }, @@ -186,16 +185,22 @@ describe("RelatedEntitiesWithSummaryComponent", () => { spyOn(TestBed.inject(EntityMapperService), "loadType").and.resolveTo( educationalData, ); + component.entity = new Child("22"); - await component.ngOnInit(); + component.ngOnInit(); + tick(); + fixture.detectChanges(); + tick(); + expect(component.summarySum).toEqual( `${PENCIL.label}: 1, ${RULER.label}: 2`, ); expect(component.data).toEqual(educationalData); - }); + })); it("should update the summary when entity updates are received", fakeAsync(() => { component.ngOnInit(); + fixture.detectChanges(); tick(); const update1 = EducationalMaterial.create({ @@ -204,6 +209,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { materialAmount: 1, }); updates.next({ entity: update1, type: "new" }); + fixture.detectChanges(); tick(); expect(component.data).toEqual([update1]); @@ -212,6 +218,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { const update2 = update1.copy() as EducationalMaterial; update2.materialAmount = 2; updates.next({ entity: update2, type: "update" }); + fixture.detectChanges(); tick(); expect(component.data).toEqual([update2]); @@ -220,6 +227,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { const unrelatedUpdate = update1.copy() as EducationalMaterial; unrelatedUpdate.child = "differentChild"; updates.next({ entity: unrelatedUpdate, type: "new" }); + fixture.detectChanges(); tick(); // No change expect(component.data).toEqual([update2]); diff --git a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts index bc0cc73248..ca2a493ae0 100644 --- a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts +++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts @@ -1,29 +1,27 @@ -import { Component, Input, OnInit } from "@angular/core"; -import { NgFor, NgIf } from "@angular/common"; +import { Component, Input, OnInit, ViewChild } from "@angular/core"; +import { NgIf } from "@angular/common"; import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { RelatedEntitiesComponent } from "../related-entities/related-entities.component"; import { Entity } from "../../entity/model/entity"; -import { filter } from "rxjs/operators"; -import { applyUpdate } from "../../entity/model/entity-update"; import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component"; /** - * Load and display a list of entity subrecords (entities related to the current entity details view) + * Load and display a list of related entities * including a summary below the table. */ @DynamicComponent("RelatedEntitiesWithSummary") -@UntilDestroy() @Component({ selector: "app-related-entities-with-summary", templateUrl: "./related-entities-with-summary.component.html", - imports: [EntitiesTableComponent, NgIf, NgFor], + imports: [EntitiesTableComponent, NgIf], standalone: true, }) export class RelatedEntitiesWithSummaryComponent extends RelatedEntitiesComponent implements OnInit { + @ViewChild(EntitiesTableComponent, { static: true }) + entitiesTable: EntitiesTableComponent; /** * Configuration of what numbers should be summarized below the table. */ @@ -37,23 +35,11 @@ export class RelatedEntitiesWithSummaryComponent summarySum = ""; summaryAvg = ""; - async ngOnInit() { + override async ngOnInit() { await super.ngOnInit(); - this.updateSummary(); - - this.entityMapper - .receiveUpdates(this.entityCtr) - .pipe( - untilDestroyed(this), - filter( - ({ entity, type }) => - type === "remove" || entity[this.property] === this.entity.getId(), - ), - ) - .subscribe((update) => { - this.data = applyUpdate(this.data, update, false); - this.updateSummary(); - }); + this.entitiesTable.filteredRecordsChange.subscribe((data) => + this.updateSummary(data), + ); } /** @@ -61,7 +47,7 @@ export class RelatedEntitiesWithSummaryComponent * The summary contains no duplicates and is in a * human-readable format */ - updateSummary() { + updateSummary(filteredData: E[]) { if (!this.summaries) { this.summarySum = ""; this.summaryAvg = ""; @@ -71,7 +57,7 @@ export class RelatedEntitiesWithSummaryComponent const summary = new Map(); const average = new Map(); - this.data.forEach((m) => { + filteredData.forEach((m) => { const amount = m[this.summaries.countProperty]; let groupLabel; if (this.summaries.groupBy) { diff --git a/src/app/core/entity-details/related-entities/related-entities.component.spec.ts b/src/app/core/entity-details/related-entities/related-entities.component.spec.ts index 6783e32efa..3b3c20aa52 100644 --- a/src/app/core/entity-details/related-entities/related-entities.component.spec.ts +++ b/src/app/core/entity-details/related-entities/related-entities.component.spec.ts @@ -10,16 +10,19 @@ import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { Child } from "../../../child-dev-project/children/model/child"; import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation"; -import { Note } from "../../../child-dev-project/notes/model/note"; import { Subject } from "rxjs"; import { UpdatedEntity } from "../../entity/model/entity-update"; import { Entity } from "../../entity/model/entity"; +import { DatabaseEntity } from "../../entity/database-entity.decorator"; +import { EntityDatatype } from "../../basic-datatypes/entity/entity.datatype"; +import { EntityArrayDatatype } from "../../basic-datatypes/entity-array/entity-array.datatype"; +import { School } from "../../../child-dev-project/schools/model/school"; +import { DatabaseField } from "../../entity/database-field.decorator"; +import { expectEntitiesToMatch } from "../../../utils/expect-entity-data.spec"; describe("RelatedEntitiesComponent", () => { - let component: RelatedEntitiesComponent; - let fixture: ComponentFixture< - RelatedEntitiesComponent - >; + let component: RelatedEntitiesComponent; + let fixture: ComponentFixture>; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -30,80 +33,63 @@ describe("RelatedEntitiesComponent", () => { RelatedEntitiesComponent, ); component = fixture.componentInstance; - component.entity = new Child(); - component.entityType = ChildSchoolRelation.ENTITY_TYPE; - component.property = "childId"; - component.columns = []; - fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); - it("should load only the entities which are linked with the passed one", async () => { - const c1 = new Child(); - const c2 = new Child(); - const r1 = new ChildSchoolRelation(); - r1.childId = c1.getId(); - const r2 = new ChildSchoolRelation(); - r2.childId = c1.getId(); - const r3 = new ChildSchoolRelation(); - r3.childId = c2.getId(); - const entityMapper = TestBed.inject(EntityMapperService); - await entityMapper.saveAll([c1, c2, r1, r2, r3]); + it("should create a filter for the passed entity", fakeAsync(() => { + const child = new Child(); const columns = ["start", "end", "schoolId"]; - const filter = { start: { $exists: true } } as any; - - component.entity = c1; + component.entity = child; component.entityType = ChildSchoolRelation.ENTITY_TYPE; - component.property = "childId"; component.columns = columns; - component.filter = filter; - await component.ngOnInit(); + fixture.detectChanges(); + tick(); - expect(component.data).toEqual([r1, r2]); - expect(component.filter).toEqual({ ...filter, childId: c1.getId() }); - }); + expect(component.filter).toEqual({ childId: child.getId() }); + })); - it("should ignore entities of the related type where the matching field is undefined instead of array", async () => { - const c1 = new Child(); - const r1 = new Note(); - r1.children = [c1.getId()]; - const rEmpty = new Note(); - delete rEmpty.children; // some entity types will not have a default empty array - const entityMapper = TestBed.inject(EntityMapperService); - await entityMapper.saveAll([c1, r1, rEmpty]); + it("should also include the provided filter", fakeAsync(() => { + const child = new Child(); + const filter = { start: { $exists: true } }; - component.entity = c1; - console.log("setting type"); - component.entityType = Note.ENTITY_TYPE; - component.property = "children"; - console.log("initializing"); - await component.ngOnInit(); + component.entity = child; + component.entityType = ChildSchoolRelation.ENTITY_TYPE; + component.filter = { ...filter }; + fixture.detectChanges(); + tick(); - expect(component.data).toEqual([r1]); - }); + expect(component.filter).toEqual({ + ...filter, + childId: child.getId(), + // added by table + isActive: true, + }); + })); - it("should create a new entity that references the related one", async () => { + it("should create a new entity that references the related one", fakeAsync(() => { const related = new Child(); component.entity = related; component.entityType = ChildSchoolRelation.ENTITY_TYPE; - component.property = "childId"; component.columns = []; - await component.ngOnInit(); + fixture.detectChanges(); + tick(); const newEntity = component.createNewRecordFactory()(); expect(newEntity instanceof ChildSchoolRelation).toBeTrue(); expect(newEntity["childId"]).toBe(related.getId()); - }); + })); it("should add a new entity that was created after the initial loading to the table", fakeAsync(() => { const entityUpdates = new Subject>(); const entityMapper = TestBed.inject(EntityMapperService); spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates); - component.ngOnInit(); + component.entity = new Child(); + component.entityType = ChildSchoolRelation.ENTITY_TYPE; + fixture.detectChanges(); tick(); const entity = new ChildSchoolRelation(); @@ -118,8 +104,10 @@ describe("RelatedEntitiesComponent", () => { const entityMapper = TestBed.inject(EntityMapperService); spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates); const entity = new ChildSchoolRelation(); + component.entity = new Child(); + component.entityType = entity.getType(); component.data = [entity]; - component.ngOnInit(); + fixture.detectChanges(); tick(); entityUpdates.next({ entity: entity, type: "remove" }); @@ -127,4 +115,109 @@ describe("RelatedEntitiesComponent", () => { expect(component.data).toEqual([]); })); + + it("should support multiple related properties", fakeAsync(() => { + @DatabaseEntity("MultiPropTest") + class MultiPropTest extends Entity { + @DatabaseField({ + dataType: EntityDatatype.dataType, + additional: Child.ENTITY_TYPE, + }) + singleChild: string; + @DatabaseField({ + dataType: EntityArrayDatatype.dataType, + additional: [Child.ENTITY_TYPE, School.ENTITY_TYPE], + }) + multiEntities: string; + } + + const child = new Child(); + component.entity = child; + component.entityType = MultiPropTest.ENTITY_TYPE; + component.filter = {}; + + fixture.detectChanges(); + tick(); + + // filter matching relations at any of the available props + expect(component.filter).toEqual({ + $or: [ + { singleChild: child.getId() }, + { multiEntities: { $elemMatch: { $eq: child.getId() } } }, + ], + // is added inside table + isActive: true, + }); + // no special properties set when creating a new entity + expectEntitiesToMatch( + [component.createNewRecordFactory()()], + [new MultiPropTest()], + true, + ); + })); + + it("should align the filter with the related properties", async () => { + @DatabaseEntity("PropTest") + class PropTest extends Entity {} + component.entityType = PropTest.ENTITY_TYPE; + + PropTest.schema.set("singleRelation", { + dataType: EntityDatatype.dataType, + additional: Child.ENTITY_TYPE, + }); + component.entity = new Child(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + expect(component.filter).toEqual({ + singleRelation: component.entity.getId(), + }); + + PropTest.schema.set("arrayRelation", { + dataType: EntityArrayDatatype.dataType, + additional: School.ENTITY_TYPE, + }); + component.entity = new School(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + expect(component.filter).toEqual({ + arrayRelation: { $elemMatch: { $eq: component.entity.getId() } }, + }); + + PropTest.schema.set("multiTypeRelation", { + dataType: EntityArrayDatatype.dataType, + additional: [ChildSchoolRelation.ENTITY_TYPE, Child.ENTITY_TYPE], + }); + component.entity = new ChildSchoolRelation(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + expect(component.filter).toEqual({ + multiTypeRelation: { $elemMatch: { $eq: component.entity.getId() } }, + }); + + // Now with 2 relations ("singleRelation" and "multiTypeRelation") + component.entity = new Child(); + component.filter = undefined; + component.property = undefined; + await component.ngOnInit(); + expect(component.filter).toEqual({ + $or: [ + { singleRelation: component.entity.getId() }, + { + multiTypeRelation: { $elemMatch: { $eq: component.entity.getId() } }, + }, + ], + }); + + // preselected property should not be changed + component.entity = new Child(); + component.filter = undefined; + component.property = "singleRelation"; + await component.ngOnInit(); + expect(component.filter).toEqual({ + singleRelation: component.entity.getId(), + }); + }); }); diff --git a/src/app/core/entity-details/related-entities/related-entities.component.ts b/src/app/core/entity-details/related-entities/related-entities.component.ts index e2c552b7c2..cba9f8414a 100644 --- a/src/app/core/entity-details/related-entities/related-entities.component.ts +++ b/src/app/core/entity-details/related-entities/related-entities.component.ts @@ -3,7 +3,6 @@ import { DynamicComponent } from "../../config/dynamic-components/dynamic-compon import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { Entity, EntityConstructor } from "../../entity/model/entity"; import { EntityRegistry } from "../../entity/database-entity.decorator"; -import { isArrayProperty } from "../../basic-datatypes/datatype-utils"; import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { applyUpdate } from "../../entity/model/entity-update"; @@ -17,6 +16,10 @@ import { toFormFieldConfig, } from "../../common-components/entity-form/FormConfig"; import { DataFilter } from "../../filter/filters/filters"; +import { FilterService } from "../../filter/filter.service"; +import { EntityDatatype } from "../../basic-datatypes/entity/entity.datatype"; +import { EntityArrayDatatype } from "../../basic-datatypes/entity-array/entity-array.datatype"; +import { isArrayProperty } from "../../basic-datatypes/datatype-utils"; /** * Load and display a list of entity subrecords (entities related to the current entity details view). @@ -39,11 +42,21 @@ export class RelatedEntitiesComponent implements OnInit { } /** - * property name of the related entities (type given in this.entityType) that holds the entity id - * to be matched with the id of the current main entity (given in this.entity) + * Property name of the related entities (type given in this.entityType) that holds the entity id + * to be matched with the id of the current main entity (given in this.entity). + * If not explicitly set, this will be inferred based on the defined relations between the entities. + * + * manually setting this is only necessary if you have multiple properties referencing the same entity type + * and you want to list only records related to one of them. + * For example: if you set `entityType = "Project"` (to display a list of projects here) and the Project entities have a properties "participants" and "supervisors" both storing references to User entities, + * you can set `property = "supervisors"` to only list those projects where the current User is supervisors, not participant. */ - @Input() property: string; + @Input() property: string | string[]; + /** + * Columns to be displayed in the table + * @param value + */ @Input() public set columns(value: ColumnConfig[]) { if (!Array.isArray(value)) { @@ -57,20 +70,26 @@ export class RelatedEntitiesComponent implements OnInit { columnsToDisplay: string[]; + /** + * This filter is applied before displaying the data. + */ @Input() filter?: DataFilter; + /** + * Whether inactive/archived records should be shown. + */ @Input() showInactive: boolean; @Input() clickMode: "popup" | "navigate" = "popup"; data: E[]; - private isArray = false; protected entityCtr: EntityConstructor; constructor( protected entityMapper: EntityMapperService, private entityRegistry: EntityRegistry, private screenWidthObserver: ScreenWidthObserver, + protected filterService: FilterService, ) { this.screenWidthObserver .shared() @@ -79,30 +98,60 @@ export class RelatedEntitiesComponent implements OnInit { } async ngOnInit() { - await this.initData(); + this.property = this.property ?? this.getProperty(); + this.data = await this.getData(); + this.filter = this.initFilter(); + + if (this.showInactive === undefined) { + // show all related docs when visiting an archived entity + this.showInactive = this.entity.anonymized; + } + this.listenToEntityUpdates(); } - protected async initData() { - this.isArray = isArrayProperty(this.entityCtr, this.property); - - this.filter = { - ...this.filter, - [this.property]: this.isArray - ? { $elemMatch: { $eq: this.entity.getId() } } - : this.entity.getId(), - }; + protected getData(): Promise { + return this.entityMapper.loadType(this.entityCtr); + } - this.data = (await this.entityMapper.loadType(this.entityCtr)).filter( - (e) => - this.isArray - ? e[this.property]?.includes(this.entity.getId()) - : e[this.property] === this.entity.getId(), + protected getProperty(): string | string[] { + const relType = this.entity.getType(); + const found = [...this.entityCtr.schema].filter( + ([, { dataType, additional }]) => { + const entityDatatype = + dataType === EntityDatatype.dataType || + dataType === EntityArrayDatatype.dataType; + return entityDatatype && Array.isArray(additional) + ? additional.includes(relType) + : additional === relType; + }, ); + return found.length === 1 ? found[0][0] : found.map(([key]) => key); + } - if (this.showInactive === undefined) { - this.showInactive = this.entity.anonymized; + protected initFilter(): DataFilter { + const filter: DataFilter = { ...this.filter }; + + if (this.property) { + // only show related entities + if (typeof this.property === "string") { + Object.assign(filter, this.getFilterForProperty(this.property)); + } else if (this.property.length > 0) { + filter["$or"] = this.property.map((prop) => + this.getFilterForProperty(prop), + ); + } } + + return filter; + } + + private getFilterForProperty(property: string) { + const isArray = isArrayProperty(this.entityCtr, property); + const filter = isArray + ? { $elemMatch: { $eq: this.entity.getId() } } + : this.entity.getId(); + return { [property]: filter }; } protected listenToEntityUpdates() { @@ -115,12 +164,9 @@ export class RelatedEntitiesComponent implements OnInit { } createNewRecordFactory() { - // TODO has a similar purpose like FilterService.alignEntityWithFilter return () => { const rec = new this.entityCtr(); - rec[this.property] = this.isArray - ? [this.entity.getId()] - : this.entity.getId(); + this.filterService.alignEntityWithFilter(rec, this.filter); return rec; }; } diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html index 14662fa85c..3582e6a7e0 100644 --- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html +++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html @@ -9,10 +9,8 @@ [records]="data" [filter]="filter" [customColumns]="_columns" - [newRecordFactory]="generateNewRecordFactory()" - [getBackgroundColor]=" - hasCurrentlyActiveEntry && showInactive ? backgroundColorFn : undefined - " + [newRecordFactory]="createNewRecordFactory()" + [getBackgroundColor]="showInactive ? backgroundColorFn : undefined" [clickMode]="clickMode" [(showInactive)]="showInactive" > diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts index a257504973..b79362ca97 100644 --- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts +++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts @@ -10,7 +10,6 @@ import { RelatedTimePeriodEntitiesComponent } from "./related-time-period-entiti import moment from "moment"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { Child } from "../../../child-dev-project/children/model/child"; -import { School } from "../../../child-dev-project/schools/model/school"; import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; @@ -24,7 +23,6 @@ describe("RelatedTimePeriodEntitiesComponent", () => { let mainEntity: Child; const entityType = "ChildSchoolRelation"; - const property = "childId"; let active1, active2, inactive: ChildSchoolRelation; @@ -56,7 +54,6 @@ describe("RelatedTimePeriodEntitiesComponent", () => { component.entity = mainEntity; component.entityType = entityType; - component.property = property; fixture.detectChanges(); }); @@ -65,22 +62,6 @@ describe("RelatedTimePeriodEntitiesComponent", () => { expect(component).toBeTruthy(); }); - it("should load correctly filtered data", async () => { - const testSchool = new School(); - active1.schoolId = testSchool.getId(); - active2.schoolId = "School:some-other-id"; - inactive.schoolId = "School:some-other-id"; - - const loadType = spyOn(entityMapper, "loadType"); - loadType.and.resolveTo([active1, active2, inactive]); - - component.entity = testSchool; - component.property = "schoolId"; - await component.ngOnInit(); - - expect(component.data).toEqual([active1]); - }); - it("should change columns to be displayed via config", async () => { component.entity = new Child(); component.single = true; @@ -116,7 +97,7 @@ describe("RelatedTimePeriodEntitiesComponent", () => { component.entity = child; await component.ngOnInit(); - const newRelation = component.generateNewRecordFactory()(); + const newRelation = component.createNewRecordFactory()(); expect(newRelation.childId).toEqual(child.getId()); }); @@ -133,7 +114,7 @@ describe("RelatedTimePeriodEntitiesComponent", () => { component.entity = child; await component.ngOnInit(); - const newRelation = component.generateNewRecordFactory()(); + const newRelation = component.createNewRecordFactory()(); expect( moment(existingRelation.end) diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts index ba1a816d96..49b05b89f2 100644 --- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts +++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts @@ -64,20 +64,10 @@ export class RelatedTimePeriodEntitiesComponent async ngOnInit() { await super.ngOnInit(); - this.onIsActiveFilterChange(); - } - - onIsActiveFilterChange() { this.hasCurrentlyActiveEntry = this.data.some((record) => record.isActive); - - if (this.showInactive) { - this.backgroundColorFn = (r: E) => r.getColor(); - } else { - this.backgroundColorFn = undefined; // Do not highlight active ones when only active are shown - } } - generateNewRecordFactory() { + override createNewRecordFactory() { return () => { const newRelation = super.createNewRecordFactory()(); diff --git a/src/app/core/filter/filter.service.spec.ts b/src/app/core/filter/filter.service.spec.ts index 40a675270a..4ec78b85f6 100644 --- a/src/app/core/filter/filter.service.spec.ts +++ b/src/app/core/filter/filter.service.spec.ts @@ -7,6 +7,8 @@ import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/co import { createTestingConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum-testing"; import moment from "moment"; import { DataFilter } from "./filters/filters"; +import { Child } from "../../child-dev-project/children/model/child"; +import { ChildSchoolRelation } from "../../child-dev-project/children/model/childSchoolRelation"; describe("FilterService", () => { let service: FilterService; @@ -67,6 +69,31 @@ describe("FilterService", () => { expect(predicate(note)).toBeTrue(); }); + it("should support patching with array values", () => { + const child = new Child(); + const filter = { + children: { $elemMatch: { $eq: child.getId() } }, + } as DataFilter; + const note = new Note(); + + service.alignEntityWithFilter(note, filter); + + expect(note.children).toEqual([child.getId()]); + }); + + it("should not set properties without a schema", () => { + const filter = { + childId: `${Child.ENTITY_TYPE}:some-id`, + isActive: false, + } as DataFilter; + + const relation = new ChildSchoolRelation(); + service.alignEntityWithFilter(relation, filter); + + expect(relation.childId).toEqual(`${Child.ENTITY_TYPE}:some-id`); + expect(relation.isActive).toBeTrue(); + }); + it("should support filtering dates with day granularity", () => { const n1 = Note.create(moment("2022-01-01").toDate()); const n2 = Note.create(moment("2022-01-02").toDate()); diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts index c4b9d49cd0..e65b8b33b1 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -66,10 +66,17 @@ export class FilterService { alignEntityWithFilter(entity: T, filter: DataFilter) { const schema = entity.getSchema(); Object.entries(filter ?? {}).forEach(([key, value]) => { - // TODO support arrays through recursion if (typeof value !== "object") { // only simple equality filters are automatically applied to new entities, complex conditions (e.g. $lt / $gt) are ignored) this.assignValueToEntity(key, value, schema, entity); + } else if (value["$elemMatch"]?.["$eq"]) { + // e.g. { children: { $elemMatch: { $eq: "Child:some-id" } } } + this.assignValueToEntity( + key, + [value["$elemMatch"]["$eq"]], + schema, + entity, + ); } }); } @@ -85,6 +92,12 @@ export class FilterService { [key, value] = this.transformNestedKey(key, value); } const property = schema.get(key); + + if (!property) { + // not a schema property + return; + } + if (property?.dataType === "configurable-enum") { value = this.parseConfigurableEnumValue(property, value); } diff --git a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts index ace2206941..ed46412877 100644 --- a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts +++ b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { HistoricalDataComponent } from "./historical-data.component"; -import { Entity } from "../../../core/entity/model/entity"; import { HistoricalEntityData } from "../model/historical-entity-data"; import moment from "moment"; import { HistoricalDataService } from "../historical-data.service"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; +import { Child } from "../../../child-dev-project/children/model/child"; describe("HistoricalDataComponent", () => { let component: HistoricalDataComponent; @@ -26,20 +26,19 @@ describe("HistoricalDataComponent", () => { }).compileComponents(); })); - beforeEach(() => { + beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(HistoricalDataComponent); component = fixture.componentInstance; - component.entity = new Entity(); + component.entity = new Child(); fixture.detectChanges(); - }); + })); it("should create", () => { expect(component).toBeTruthy(); }); it("should load the historical data", async () => { - component.entity = new Entity(); const relatedData = new HistoricalEntityData(); relatedData.relatedEntity = component.entity.getId(); mockHistoricalDataService.getHistoricalDataFor.and.resolveTo([relatedData]); @@ -53,9 +52,7 @@ describe("HistoricalDataComponent", () => { }); it("should generate new records with a link to the passed entity", () => { - component.entity = new Entity(); - - const newEntry = component.getNewEntryFunction()(); + const newEntry = component.createNewRecordFactory()(); expect(newEntry.relatedEntity).toBe(component.entity.getId()); expect(moment(newEntry.date).isSame(new Date(), "day")).toBeTrue(); diff --git a/src/app/features/historical-data/historical-data/historical-data.component.ts b/src/app/features/historical-data/historical-data/historical-data.component.ts index faf99d28a1..9542aeb66a 100644 --- a/src/app/features/historical-data/historical-data/historical-data.component.ts +++ b/src/app/features/historical-data/historical-data/historical-data.component.ts @@ -9,6 +9,7 @@ import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-m import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig"; +import { FilterService } from "../../../core/filter/filter.service"; /** * A general component that can be included on a entity details page through the config. @@ -28,7 +29,6 @@ export class HistoricalDataComponent implements OnInit { @Input() entity: Entity; - property = "relatedEntity"; entityCtr = HistoricalEntityData; /** @deprecated use @Input() columns instead */ @@ -43,21 +43,12 @@ export class HistoricalDataComponent entityMapper: EntityMapperService, entityRegistry: EntityRegistry, screenWidthObserver: ScreenWidthObserver, + filterService: FilterService, ) { - super(entityMapper, entityRegistry, screenWidthObserver); + super(entityMapper, entityRegistry, screenWidthObserver, filterService); } - override async initData() { - this.data = await this.historicalDataService.getHistoricalDataFor( - this.entity.getId(), - ); - } - - public getNewEntryFunction(): () => HistoricalEntityData { - return () => { - const newEntry = new HistoricalEntityData(); - newEntry.relatedEntity = this.entity.getId(); - return newEntry; - }; + override getData() { + return this.historicalDataService.getHistoricalDataFor(this.entity.getId()); } } diff --git a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.spec.ts b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.spec.ts index cf98c86123..fcb2f2da5f 100644 --- a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.spec.ts +++ b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.spec.ts @@ -1,42 +1,108 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { TodosRelatedToEntityComponent } from "./todos-related-to-entity.component"; -import { DatabaseIndexingService } from "../../../core/entity/database-indexing/database-indexing.service"; -import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { Entity } from "../../../core/entity/model/entity"; +import { DatabaseTestingModule } from "../../../utils/database-testing.module"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { Todo } from "../model/todo"; +import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; +import { School } from "../../../child-dev-project/schools/model/school"; +import { User } from "../../../core/user/user"; +import { Database } from "../../../core/database/database"; +import { DatabaseIndexingService } from "../../../core/entity/database-indexing/database-indexing.service"; describe("TodosRelatedToEntityComponent", () => { let component: TodosRelatedToEntityComponent; let fixture: ComponentFixture; - let mockIndexingService: jasmine.SpyObj; - - beforeEach(async () => { - mockIndexingService = jasmine.createSpyObj([ - "generateIndexOnProperty", - "queryIndexDocs", - ]); - mockIndexingService.queryIndexDocs.and.resolveTo([]); - - await TestBed.configureTestingModule({ - imports: [TodosRelatedToEntityComponent, MockedTestingModule.withState()], - providers: [ - { - provide: DatabaseIndexingService, - useValue: mockIndexingService, - }, - ], + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TodosRelatedToEntityComponent, DatabaseTestingModule], }).compileComponents(); + })); + beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(TodosRelatedToEntityComponent); component = fixture.componentInstance; component.entity = new Entity(); fixture.detectChanges(); - }); + })); + + afterEach(() => TestBed.inject(Database).destroy()); it("should create", () => { expect(component).toBeTruthy(); }); + + it("should load data from index when having a single relation", async () => { + const child = new Child(); + const relatedTodo = new Todo(); + relatedTodo.relatedEntities = [child.getId(), new School().getId()]; + const unrelatedTodo = new Todo(); + unrelatedTodo.relatedEntities = [new Child().getId()]; + await TestBed.inject(EntityMapperService).saveAll([ + relatedTodo, + unrelatedTodo, + ]); + const indexSpy = spyOn( + TestBed.inject(DatabaseIndexingService), + "queryIndexDocs", + ).and.callThrough(); + + component.entity = child; + component.property = undefined; + component.filter = undefined; + await component.ngOnInit(); + + expect(indexSpy).toHaveBeenCalled(); + expect(component.filter).toEqual({ + relatedEntities: { $elemMatch: { $eq: child.getId() } }, + }); + expect(component.data).toEqual([relatedTodo]); + }); + + it("should load data with entity mapper when having multiple relations", async () => { + const relatedSchema = Todo.schema.get("relatedEntities"); + const originalAdditional = relatedSchema.additional; + relatedSchema.additional = [User.ENTITY_TYPE]; + const user = new User(); + const relatedTodo = new Todo(); + relatedTodo.relatedEntities = [user.getId()]; + const relatedTodo2 = new Todo(); + relatedTodo2.assignedTo = [user.getId()]; + relatedTodo2.relatedEntities = [new User().getId()]; + const unrelatedTodo = new Todo(); + unrelatedTodo.relatedEntities = [new User().getId()]; + const entityMapper = TestBed.inject(EntityMapperService); + await entityMapper.saveAll([relatedTodo, relatedTodo2, unrelatedTodo]); + const loadTypeSpy = spyOn(entityMapper, "loadType").and.callThrough(); + + component.entity = user; + component.property = undefined; + component.filter = undefined; + await component.ngOnInit(); + + expect(loadTypeSpy).toHaveBeenCalledWith(Todo); + expect(component.data).toEqual( + jasmine.arrayWithExactContents([ + relatedTodo, + relatedTodo2, + unrelatedTodo, + ]), + ); + expect(component.filter).toEqual({ + $or: [ + { + assignedTo: { $elemMatch: { $eq: user.getId() } }, + }, + { + relatedEntities: { $elemMatch: { $eq: user.getId() } }, + }, + ], + }); + + relatedSchema.additional = originalAdditional; + }); }); diff --git a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts index d4e58385f8..ab630b27f6 100644 --- a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts +++ b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts @@ -13,6 +13,7 @@ import { RelatedEntitiesComponent } from "../../../core/entity-details/related-e import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; +import { FilterService } from "../../../core/filter/filter.service"; @DynamicComponent("TodosRelatedToEntity") @Component({ @@ -35,9 +36,6 @@ export class TodosRelatedToEntityComponent extends RelatedEntitiesComponent custom filter component or some kind of variable interpolation? override filter: DataFilter = { isActive: true }; backgroundColorFn = (r: Todo) => { @@ -53,26 +51,28 @@ export class TodosRelatedToEntityComponent extends RelatedEntitiesComponent { + const entityId = this.entity.getId(); return this.dbIndexingService.queryIndexDocs( Todo, - "todo_index/by_" + this.referenceProperty, + "todo_index/by_" + this.property, { startkey: [entityId, "\uffff"], endkey: [entityId],