From 1efcc2e913f4320bfa4f22e5475d5cc115df38f9 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 13 Oct 2023 10:35:23 +0200 Subject: [PATCH 1/5] feat(core): generalize related entities with summary component to be usable with any entity type, not just EducationalMaterial closes #2004 --- .../educational-material.component.html | 15 +- .../educational-material.component.spec.ts | 72 +++++++--- .../educational-material.component.ts | 132 ++++++++---------- .../related-entities.component.ts | 2 +- 4 files changed, 117 insertions(+), 104 deletions(-) diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html index abfd833c5c..731f68d9e4 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html +++ b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html @@ -1,15 +1,16 @@ - + [records]="data" + [filter]="filter" + [columns]="columns" + [newRecordFactory]="createNewRecordFactory()" + [isLoading]="isLoading" +>
- Total: {{ summary }}
+ Total: {{ summary }}
- Average: {{ avgSummary }}
+ Average: {{ avgSummary }}
diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts index 9068e33c8e..4e546a1b83 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts +++ b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts @@ -1,4 +1,10 @@ -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from "@angular/core/testing"; import { EducationalMaterialComponent } from "./educational-material.component"; import { Child } from "../../model/child"; @@ -35,7 +41,16 @@ describe("EducationalMaterialComponent", () => { fixture = TestBed.createComponent(EducationalMaterialComponent); component = fixture.componentInstance; component.entity = child; - component.summaries = {total: true, average: true}; + component.entityType = EducationalMaterial.ENTITY_TYPE; + component.property = "child"; + + component.summaries = { + countProperty: "materialAmount", + groupBy: "materialType", + total: true, + average: true, + }; + fixture.detectChanges(); }); @@ -44,7 +59,7 @@ describe("EducationalMaterialComponent", () => { }); it("produces an empty summary when there are no records", () => { - component.records = []; + component.data = []; component.updateSummary(); expect(component.summary).toHaveSize(0); expect(component.avgSummary).toHaveSize(0); @@ -53,7 +68,7 @@ describe("EducationalMaterialComponent", () => { function setRecordsAndGenerateSummary( ...records: Partial[] ) { - component.records = records.map(EducationalMaterial.create); + component.data = records.map(EducationalMaterial.create); component.updateSummary(); } @@ -69,7 +84,9 @@ describe("EducationalMaterialComponent", () => { { materialType: RULER, materialAmount: 1 }, ); expect(component.summary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); + expect(component.avgSummary).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1`, + ); }); it("produces a summary of all records when there are duplicates", () => { @@ -80,11 +97,14 @@ describe("EducationalMaterialComponent", () => { ); expect(component.summary).toEqual(`${PENCIL.label}: 4, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); + expect(component.avgSummary).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1`, + ); }); it("produces summary of all records when average is false and total is true", () => { - component.summaries = { total: true, average: false } + component.summaries.total = true; + component.summaries.average = false; setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 1 }, { materialType: RULER, materialAmount: 1 }, @@ -96,7 +116,8 @@ describe("EducationalMaterialComponent", () => { }); it("produces summary of all records when average is true and total is false", () => { - component.summaries = { total: false, average: true }; + component.summaries.total = false; + component.summaries.average = true; setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 1 }, { materialType: RULER, materialAmount: 1 }, @@ -104,23 +125,25 @@ describe("EducationalMaterialComponent", () => { ); expect(component.summary).toEqual(``); - expect(component.avgSummary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); + expect(component.avgSummary).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1`, + ); }); it("does not produces summary of all records when both average and total are false", () => { - component.summaries = { total: false, average: false }; + component.summaries.total = false; + component.summaries.average = false; setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 1 }, { materialType: RULER, materialAmount: 1 }, { materialType: PENCIL, materialAmount: 3 }, ); - + expect(component.summary).toEqual(``); expect(component.avgSummary).toEqual(``); }); it("produces summary of all records when both average and total are true", () => { - component.summaries = { total: true, average: true }; setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 1 }, { materialType: RULER, materialAmount: 1 }, @@ -128,7 +151,9 @@ describe("EducationalMaterialComponent", () => { ); expect(component.summary).toEqual(`${PENCIL.label}: 4, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); + expect(component.avgSummary).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1`, + ); }); it("loads all education data associated with a child and updates the summary", async () => { @@ -142,37 +167,38 @@ describe("EducationalMaterialComponent", () => { component.entity = new Child("22"); await component.ngOnInit(); expect(component.summary).toEqual(`${PENCIL.label}: 1, ${RULER.label}: 2`); - expect(component.records).toEqual(educationalData); + expect(component.data).toEqual(educationalData); }); - it("associates a new record with the current child", () => { - const newRecord = component.newRecordFactory(); - expect(newRecord.child).toBe(child.getId()); - }); + it("should update the summary when entity updates are received", fakeAsync(() => { + component.ngOnInit(); + tick(); - it("should update the summary when entity updates are received", async () => { const update1 = EducationalMaterial.create({ child: child.getId(), materialType: PENCIL, materialAmount: 1, }); updates.next({ entity: update1, type: "new" }); + tick(); - expect(component.records).toEqual([update1]); + expect(component.data).toEqual([update1]); expect(component.summary).toBe(`${PENCIL.label}: 1`); const update2 = update1.copy() as EducationalMaterial; update2.materialAmount = 2; updates.next({ entity: update2, type: "update" }); + tick(); - expect(component.records).toEqual([update2]); + expect(component.data).toEqual([update2]); expect(component.summary).toBe(`${PENCIL.label}: 2`); const unrelatedUpdate = update1.copy() as EducationalMaterial; unrelatedUpdate.child = "differentChild"; updates.next({ entity: unrelatedUpdate, type: "new" }); + tick(); // No change - expect(component.records).toEqual([update2]); + expect(component.data).toEqual([update2]); expect(component.summary).toBe(`${PENCIL.label}: 2`); - }); + })); }); diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts index 5a065632c3..ecd1ab50fd 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts +++ b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts @@ -1,120 +1,106 @@ import { Component, Input, OnInit } from "@angular/core"; import { NgFor, NgIf } from "@angular/common"; -import { EducationalMaterial } from "../model/educational-material"; -import { Child } from "../../model/child"; -import { FormFieldConfig } from "../../../../core/common-components/entity-form/entity-form/FormConfig"; import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; -import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service"; -import { applyUpdate } from "../../../../core/entity/model/entity-update"; -import { filter } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { EntitySubrecordComponent } from "../../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component"; +import { RelatedEntitiesComponent } from "../../../../core/entity-details/related-entities/related-entities.component"; +import { Entity } from "../../../../core/entity/model/entity"; +import { filter } from "rxjs/operators"; +import { applyUpdate } from "../../../../core/entity/model/entity-update"; /** - * Displays educational materials of a child, such as a pencil, rulers, e.t.c - * as well as a summary + * Load and display a list of entity subrecords (entities related to the current entity details view) + * including a summary below the table. */ @DynamicComponent("EducationalMaterial") @UntilDestroy() @Component({ selector: "app-educational-material", templateUrl: "./educational-material.component.html", - imports: [ - EntitySubrecordComponent, - NgIf, - NgFor - ], + imports: [EntitySubrecordComponent, NgIf, NgFor], standalone: true, }) -export class EducationalMaterialComponent implements OnInit { - @Input() entity: Child; - @Input() summaries: { total?: boolean; average?: boolean } = { total: true }; - records: EducationalMaterial[] = []; +export class EducationalMaterialComponent + extends RelatedEntitiesComponent + implements OnInit +{ + /** + * Configuration of what numbers should be summarized below the table. + */ + @Input() summaries?: { + countProperty: string; + groupBy?: string; + total?: boolean; + average?: boolean; + }; + summary = ""; avgSummary = ""; - @Input() config: { columns: FormFieldConfig[] } = { - columns: [ - { id: "date", visibleFrom: "xs" }, - { id: "materialType", visibleFrom: "xs" }, - { id: "materialAmount", visibleFrom: "md" }, - { id: "description", visibleFrom: "md" }, - ], - }; + async ngOnInit() { + await super.ngOnInit(); + this.updateSummary(); - constructor(private entityMapper: EntityMapperService) { this.entityMapper - .receiveUpdates(EducationalMaterial) + .receiveUpdates(this.entityCtr) .pipe( untilDestroyed(this), filter( ({ entity, type }) => - type === "remove" || entity.child === this.entity.getId(), + type === "remove" || entity[this.property] === this.entity.getId(), ), ) .subscribe((update) => { - this.records = applyUpdate(this.records, update); + this.data = applyUpdate(this.data, update); this.updateSummary(); }); } - ngOnInit() { - return this.loadData(); - } - - /** - * Loads the data for a given child and updates the summary - * @param id The id of the child to load the data for - */ - private async loadData() { - const allMaterials = await this.entityMapper.loadType(EducationalMaterial); - this.records = allMaterials.filter( - (mat) => mat.child === this.entity.getId(), - ); - this.updateSummary(); - } - - newRecordFactory = () => { - const newAtt = new EducationalMaterial(Date.now().toString()); - - // use last entered date as default, otherwise today's date - newAtt.date = this.records.length > 0 ? this.records[0].date : new Date(); - newAtt.child = this.entity.getId(); - - return newAtt; - }; - /** * update the summary or generate a new one. * The summary contains no duplicates and is in a * human-readable format */ updateSummary() { + if (!this.summaries) { + this.summary = ""; + this.avgSummary = ""; + return; + } + const summary = new Map(); const average = new Map(); - this.records.forEach((m) => { - const { materialType, materialAmount } = m; - const label = materialType?.label; - - if (label) { - summary.set(label, (summary.get(label) || { count: 0, sum: 0 })); - summary.get(label)!.count++; - summary.get(label)!.sum += materialAmount; + this.data.forEach((m) => { + const amount = m[this.summaries.countProperty]; + let groupLabel; + if (this.summaries.groupBy) { + groupLabel = + m[this.summaries.groupBy]?.label ?? m[this.summaries.groupBy]; } + + summary.set(groupLabel, summary.get(groupLabel) || { count: 0, sum: 0 }); + summary.get(groupLabel)!.count++; + summary.get(groupLabel)!.sum += amount; }); - - if(this.summaries.total) { - const summaryArray = Array.from(summary.entries(), ([label, { sum }]) => `${label}: ${sum}`); + + if (this.summaries.total) { + const summaryArray = Array.from( + summary.entries(), + ([label, { sum }]) => `${label}: ${sum}`, + ); this.summary = summaryArray.join(", "); } - - if(this.summaries.average) { - const avgSummaryArray = Array.from(summary.entries(), ([label, { count, sum }]) => { - const avg = parseFloat((sum / count).toFixed(2)); - average.set(label, avg); - return `${label}: ${avg}`; - }); + + if (this.summaries.average) { + const avgSummaryArray = Array.from( + summary.entries(), + ([label, { count, sum }]) => { + const avg = parseFloat((sum / count).toFixed(2)); + average.set(label, avg); + return `${label}: ${avg}`; + }, + ); this.avgSummary = avgSummaryArray.join(", "); } } 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 a6c9b1507a..b4fc010fdf 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 @@ -50,7 +50,7 @@ export class RelatedEntitiesComponent implements OnInit { protected entityCtr: EntityConstructor; constructor( - private entityMapper: EntityMapperService, + protected entityMapper: EntityMapperService, private entities: EntityRegistry, ) {} From c21d9e8cae6a7a12c38ef456c060e53bcd91cf2c Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 13 Oct 2023 10:44:55 +0200 Subject: [PATCH 2/5] rename --- .../children/children-components.ts | 6 ++--- src/app/core/config/config-fix.ts | 20 ++++++++++++---- ...ated-entities-with-summary.component.html} | 0 ...d-entities-with-summary.component.spec.ts} | 24 +++++++++---------- ...elated-entities-with-summary.component.ts} | 18 +++++++------- 5 files changed, 39 insertions(+), 29 deletions(-) rename src/app/{child-dev-project/children/educational-material/educational-material-component/educational-material.component.html => core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html} (100%) rename src/app/{child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts => core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts} (86%) rename src/app/{child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts => core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts} (78%) diff --git a/src/app/child-dev-project/children/children-components.ts b/src/app/child-dev-project/children/children-components.ts index bf7e52a181..569caea244 100644 --- a/src/app/child-dev-project/children/children-components.ts +++ b/src/app/child-dev-project/children/children-components.ts @@ -45,11 +45,11 @@ export const childrenComponents: ComponentTuple[] = [ ).then((c) => c.ChildrenBmiDashboardComponent), ], [ - "EducationalMaterial", + "RelatedEntitiesWithSummary", () => import( - "./educational-material/educational-material-component/educational-material.component" - ).then((c) => c.EducationalMaterialComponent), + "../../core/entity-details/related-entities-with-summary/related-entities-with-summary.component" + ).then((c) => c.RelatedEntitiesWithSummaryComponent), ], [ "BmiBlock", diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 333e83ca6f..0c160bf969 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -706,14 +706,24 @@ export const defaultJsonConfig = { "components": [ { "title": "", - "component": "EducationalMaterial", + "component": "RelatedEntitiesWithSummary", "config": { - "summaries": { - total: true, - average: true, + "entityType": EducationalMaterial.ENTITY_TYPE, + "property": "child", + "columns": [ + { "id": "date", "visibleFrom": "xs" }, + { "id": "materialType", "visibleFrom": "xs" }, + { "id": "materialAmount", "visibleFrom": "md" }, + { "id": "description", "visibleFrom": "md" }, + ], + "summaries": { + "countProperty": "materialAmount", + "groupBy": "materialType", + "total": true, + "average": false } } - } + } ] }, { diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html similarity index 100% rename from src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.html rename to src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts similarity index 86% rename from src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts rename to src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts index 4e546a1b83..40c3cd6871 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.spec.ts +++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.spec.ts @@ -6,18 +6,18 @@ import { waitForAsync, } from "@angular/core/testing"; -import { EducationalMaterialComponent } from "./educational-material.component"; -import { Child } from "../../model/child"; -import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; -import { EducationalMaterial } from "../model/educational-material"; -import { ConfigurableEnumValue } from "../../../../core/basic-datatypes/configurable-enum/configurable-enum.interface"; -import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service"; +import { RelatedEntitiesWithSummaryComponent } from "./related-entities-with-summary.component"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import { EducationalMaterial } from "../../../child-dev-project/children/educational-material/model/educational-material"; +import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface"; +import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { Subject } from "rxjs"; -import { UpdatedEntity } from "../../../../core/entity/model/entity-update"; +import { UpdatedEntity } from "../../entity/model/entity-update"; -describe("EducationalMaterialComponent", () => { - let component: EducationalMaterialComponent; - let fixture: ComponentFixture; +describe("RelatedEntitiesWithSummaryComponent", () => { + let component: RelatedEntitiesWithSummary; + let fixture: ComponentFixture; const updates = new Subject>(); const child = new Child("22"); const PENCIL: ConfigurableEnumValue = { @@ -31,14 +31,14 @@ describe("EducationalMaterialComponent", () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [EducationalMaterialComponent, MockedTestingModule.withState()], + imports: [RelatedEntitiesWithSummary, MockedTestingModule.withState()], }).compileComponents(); const entityMapper = TestBed.inject(EntityMapperService); spyOn(entityMapper, "receiveUpdates").and.returnValue(updates); })); beforeEach(() => { - fixture = TestBed.createComponent(EducationalMaterialComponent); + fixture = TestBed.createComponent(RelatedEntitiesWithSummary); component = fixture.componentInstance; component.entity = child; component.entityType = EducationalMaterial.ENTITY_TYPE; diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts similarity index 78% rename from src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts rename to src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts index ecd1ab50fd..adce39425b 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts +++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts @@ -1,26 +1,26 @@ import { Component, Input, OnInit } from "@angular/core"; import { NgFor, NgIf } from "@angular/common"; -import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; +import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { EntitySubrecordComponent } from "../../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component"; -import { RelatedEntitiesComponent } from "../../../../core/entity-details/related-entities/related-entities.component"; -import { Entity } from "../../../../core/entity/model/entity"; +import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component"; +import { RelatedEntitiesComponent } from "../related-entities/related-entities.component"; +import { Entity } from "../../entity/model/entity"; import { filter } from "rxjs/operators"; -import { applyUpdate } from "../../../../core/entity/model/entity-update"; +import { applyUpdate } from "../../entity/model/entity-update"; /** * Load and display a list of entity subrecords (entities related to the current entity details view) * including a summary below the table. */ -@DynamicComponent("EducationalMaterial") +@DynamicComponent("RelatedEntitiesWithSummary") @UntilDestroy() @Component({ - selector: "app-educational-material", - templateUrl: "./educational-material.component.html", + selector: "app-related-entities-with-summary", + templateUrl: "./related-entities-with-summary.component.html", imports: [EntitySubrecordComponent, NgIf, NgFor], standalone: true, }) -export class EducationalMaterialComponent +export class RelatedEntitiesWithSummaryComponent extends RelatedEntitiesComponent implements OnInit { From b46968692b08a51af0f33a0663f262d6fd80f383 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 13 Oct 2023 10:51:48 +0200 Subject: [PATCH 3/5] fix data not being filtered for entity --- .../related-entities-with-summary.component.spec.ts | 11 +++++++---- .../related-entities.component.spec.ts | 4 ++-- .../related-entities/related-entities.component.ts | 7 ++++++- .../related-time-period-entities.component.spec.ts | 3 ++- 4 files changed, 17 insertions(+), 8 deletions(-) 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 40c3cd6871..2c876a4bc9 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 @@ -16,8 +16,8 @@ import { Subject } from "rxjs"; import { UpdatedEntity } from "../../entity/model/entity-update"; describe("RelatedEntitiesWithSummaryComponent", () => { - let component: RelatedEntitiesWithSummary; - let fixture: ComponentFixture; + let component: RelatedEntitiesWithSummaryComponent; + let fixture: ComponentFixture; const updates = new Subject>(); const child = new Child("22"); const PENCIL: ConfigurableEnumValue = { @@ -31,14 +31,17 @@ describe("RelatedEntitiesWithSummaryComponent", () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [RelatedEntitiesWithSummary, MockedTestingModule.withState()], + imports: [ + RelatedEntitiesWithSummaryComponent, + MockedTestingModule.withState(), + ], }).compileComponents(); const entityMapper = TestBed.inject(EntityMapperService); spyOn(entityMapper, "receiveUpdates").and.returnValue(updates); })); beforeEach(() => { - fixture = TestBed.createComponent(RelatedEntitiesWithSummary); + fixture = TestBed.createComponent(RelatedEntitiesWithSummaryComponent); component = fixture.componentInstance; component.entity = child; component.entityType = EducationalMaterial.ENTITY_TYPE; 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 44c9b0eebd..49a1a1f2f5 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 @@ -30,7 +30,7 @@ describe("RelatedEntitiesComponent", () => { expect(component).toBeTruthy(); }); - it("should load the entities which are linked with the passed one", async () => { + 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(); @@ -52,7 +52,7 @@ describe("RelatedEntitiesComponent", () => { await component.ngOnInit(); expect(component.columns).toBe(columns); - expect(component.data).toEqual([r1, r2, r3]); + expect(component.data).toEqual([r1, r2]); expect(component.filter).toEqual({ ...filter, childId: c1.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 b4fc010fdf..7e5a4eb93e 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 @@ -64,7 +64,12 @@ export class RelatedEntitiesComponent implements OnInit { this.entityCtr = this.entities.get(this.entityType) as EntityConstructor; this.isArray = isArrayProperty(this.entityCtr, this.property); - this.data = await this.entityMapper.loadType(this.entityType); + this.data = (await this.entityMapper.loadType(this.entityType)).filter( + (e) => + this.isArray + ? e[this.property].includes(this.entity.getId()) + : e[this.property] === this.entity.getId(), + ); this.filter = { ...this.filter, [this.property]: this.isArray 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 85bcc2e268..e5ad40422f 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 @@ -124,13 +124,14 @@ describe("RelatedTimePeriodEntitiesComponent", () => { }); it("should create a new entity with the start date inferred from previous relations", async () => { + const child = new Child(); const existingRelation = new ChildSchoolRelation(); existingRelation.start = moment().subtract(1, "year").toDate(); existingRelation.end = moment().subtract(1, "week").toDate(); + existingRelation.childId = child.getId(false); const loadType = spyOn(entityMapper, "loadType"); loadType.and.resolveTo([existingRelation]); - const child = new Child(); component.entity = child; await component.ngOnInit(); From b3670a9ee6139ebe5b77ff729af75b31abafeac8 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Mon, 16 Oct 2023 10:49:55 +0200 Subject: [PATCH 4/5] rename vars and add handling for case without groupBy --- ...lated-entities-with-summary.component.html | 8 +-- ...ed-entities-with-summary.component.spec.ts | 63 ++++++++++++------- ...related-entities-with-summary.component.ts | 22 ++++--- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html index 731f68d9e4..c59e53e42c 100644 --- a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html +++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html @@ -7,10 +7,10 @@ >
- - Total: {{ summary }}
+ + Total: {{ summarySum }}
- - Average: {{ avgSummary }}
+ + Average: {{ summaryAvg }}
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 2c876a4bc9..5c17e54951 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 @@ -64,8 +64,8 @@ describe("RelatedEntitiesWithSummaryComponent", () => { it("produces an empty summary when there are no records", () => { component.data = []; component.updateSummary(); - expect(component.summary).toHaveSize(0); - expect(component.avgSummary).toHaveSize(0); + expect(component.summarySum).toHaveSize(0); + expect(component.summaryAvg).toHaveSize(0); }); function setRecordsAndGenerateSummary( @@ -77,21 +77,34 @@ describe("RelatedEntitiesWithSummaryComponent", () => { it("produces a singleton summary when there is a single record", () => { setRecordsAndGenerateSummary({ materialType: PENCIL, materialAmount: 1 }); - expect(component.summary).toEqual(`${PENCIL.label}: 1`); - expect(component.avgSummary).toEqual(`${PENCIL.label}: 1`); + expect(component.summarySum).toEqual(`${PENCIL.label}: 1`); + expect(component.summaryAvg).toEqual(`${PENCIL.label}: 1`); }); it("produces a summary of all records when they are all different", () => { setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 2 }, { materialType: RULER, materialAmount: 1 }, + { materialAmount: 1 }, ); - expect(component.summary).toEqual(`${PENCIL.label}: 2, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual( - `${PENCIL.label}: 2, ${RULER.label}: 1`, + expect(component.summarySum).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1, undefined: 1`, + ); + expect(component.summaryAvg).toEqual( + `${PENCIL.label}: 2, ${RULER.label}: 1, undefined: 1`, ); }); + it("produces a singly summary without grouping, if `groupBy` is not given (or the group value undefined)", () => { + component.data = [{ amount: 1 }, { amount: 5 }] as any[]; + delete component.summaries.groupBy; + component.summaries.countProperty = "amount"; + component.updateSummary(); + + expect(component.summarySum).toEqual(`6`); + expect(component.summaryAvg).toEqual(`3`); + }); + it("produces a summary of all records when there are duplicates", () => { setRecordsAndGenerateSummary( { materialType: PENCIL, materialAmount: 1 }, @@ -99,8 +112,10 @@ describe("RelatedEntitiesWithSummaryComponent", () => { { materialType: PENCIL, materialAmount: 3 }, ); - expect(component.summary).toEqual(`${PENCIL.label}: 4, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual( + expect(component.summarySum).toEqual( + `${PENCIL.label}: 4, ${RULER.label}: 1`, + ); + expect(component.summaryAvg).toEqual( `${PENCIL.label}: 2, ${RULER.label}: 1`, ); }); @@ -114,8 +129,10 @@ describe("RelatedEntitiesWithSummaryComponent", () => { { materialType: PENCIL, materialAmount: 3 }, ); - expect(component.summary).toEqual(`${PENCIL.label}: 4, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual(``); + expect(component.summarySum).toEqual( + `${PENCIL.label}: 4, ${RULER.label}: 1`, + ); + expect(component.summaryAvg).toEqual(``); }); it("produces summary of all records when average is true and total is false", () => { @@ -127,8 +144,8 @@ describe("RelatedEntitiesWithSummaryComponent", () => { { materialType: PENCIL, materialAmount: 3 }, ); - expect(component.summary).toEqual(``); - expect(component.avgSummary).toEqual( + expect(component.summarySum).toEqual(``); + expect(component.summaryAvg).toEqual( `${PENCIL.label}: 2, ${RULER.label}: 1`, ); }); @@ -142,8 +159,8 @@ describe("RelatedEntitiesWithSummaryComponent", () => { { materialType: PENCIL, materialAmount: 3 }, ); - expect(component.summary).toEqual(``); - expect(component.avgSummary).toEqual(``); + expect(component.summarySum).toEqual(``); + expect(component.summaryAvg).toEqual(``); }); it("produces summary of all records when both average and total are true", () => { @@ -153,8 +170,10 @@ describe("RelatedEntitiesWithSummaryComponent", () => { { materialType: PENCIL, materialAmount: 3 }, ); - expect(component.summary).toEqual(`${PENCIL.label}: 4, ${RULER.label}: 1`); - expect(component.avgSummary).toEqual( + expect(component.summarySum).toEqual( + `${PENCIL.label}: 4, ${RULER.label}: 1`, + ); + expect(component.summaryAvg).toEqual( `${PENCIL.label}: 2, ${RULER.label}: 1`, ); }); @@ -169,7 +188,9 @@ describe("RelatedEntitiesWithSummaryComponent", () => { ); component.entity = new Child("22"); await component.ngOnInit(); - expect(component.summary).toEqual(`${PENCIL.label}: 1, ${RULER.label}: 2`); + expect(component.summarySum).toEqual( + `${PENCIL.label}: 1, ${RULER.label}: 2`, + ); expect(component.data).toEqual(educationalData); }); @@ -186,7 +207,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { tick(); expect(component.data).toEqual([update1]); - expect(component.summary).toBe(`${PENCIL.label}: 1`); + expect(component.summarySum).toBe(`${PENCIL.label}: 1`); const update2 = update1.copy() as EducationalMaterial; update2.materialAmount = 2; @@ -194,7 +215,7 @@ describe("RelatedEntitiesWithSummaryComponent", () => { tick(); expect(component.data).toEqual([update2]); - expect(component.summary).toBe(`${PENCIL.label}: 2`); + expect(component.summarySum).toBe(`${PENCIL.label}: 2`); const unrelatedUpdate = update1.copy() as EducationalMaterial; unrelatedUpdate.child = "differentChild"; @@ -202,6 +223,6 @@ describe("RelatedEntitiesWithSummaryComponent", () => { tick(); // No change expect(component.data).toEqual([update2]); - expect(component.summary).toBe(`${PENCIL.label}: 2`); + expect(component.summarySum).toBe(`${PENCIL.label}: 2`); })); }); 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 adce39425b..ea32c4bec6 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 @@ -34,8 +34,8 @@ export class RelatedEntitiesWithSummaryComponent average?: boolean; }; - summary = ""; - avgSummary = ""; + summarySum = ""; + summaryAvg = ""; async ngOnInit() { await super.ngOnInit(); @@ -63,8 +63,8 @@ export class RelatedEntitiesWithSummaryComponent */ updateSummary() { if (!this.summaries) { - this.summary = ""; - this.avgSummary = ""; + this.summarySum = ""; + this.summaryAvg = ""; return; } @@ -85,15 +85,15 @@ export class RelatedEntitiesWithSummaryComponent }); if (this.summaries.total) { - const summaryArray = Array.from( + const summarySumArray = Array.from( summary.entries(), ([label, { sum }]) => `${label}: ${sum}`, ); - this.summary = summaryArray.join(", "); + this.summarySum = summarySumArray.join(", "); } if (this.summaries.average) { - const avgSummaryArray = Array.from( + const summaryAvgArray = Array.from( summary.entries(), ([label, { count, sum }]) => { const avg = parseFloat((sum / count).toFixed(2)); @@ -101,7 +101,13 @@ export class RelatedEntitiesWithSummaryComponent return `${label}: ${avg}`; }, ); - this.avgSummary = avgSummaryArray.join(", "); + this.summaryAvg = summaryAvgArray.join(", "); + } + + if (summary.size === 1 && summary.has(undefined)) { + // display only single summary without group label (this also applies if no groupBy is given) + this.summarySum = this.summarySum.replace("undefined: ", ""); + this.summaryAvg = this.summaryAvg.replace("undefined: ", ""); } } } From 38974eab0bbc68f1ea732da738a9f9cc37815fb3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 13:17:03 +0200 Subject: [PATCH 5/5] Update src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts Co-authored-by: Simon --- .../related-entities-with-summary.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ea32c4bec6..2055435762 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 @@ -80,8 +80,8 @@ export class RelatedEntitiesWithSummaryComponent } summary.set(groupLabel, summary.get(groupLabel) || { count: 0, sum: 0 }); - summary.get(groupLabel)!.count++; - summary.get(groupLabel)!.sum += amount; + summary.get(groupLabel).count++; + summary.get(groupLabel).sum += amount; }); if (this.summaries.total) {