diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 105602d036..e0f3844a20 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -676,4 +676,36 @@ describe("ConfigService", () => { }, ); })); + + it("should wrap groupBy as an array if it is a string", fakeAsync(() => { + const oldConfig = { + component: "EntityCountDashboard", + config: { + entityType: "Child", + groupBy: "center", // groupBy is a string + }, + }; + + const expectedNewConfig = { + component: "EntityCountDashboard", + config: { + entityType: "Child", + groupBy: ["center"], // groupBy should be wrapped as an array + }, + }; + + testConfigMigration(oldConfig, expectedNewConfig); + + // should not change other configs that have a groupBy property + const otherConfig = { + "view:X": { + config: { + columns: { + groupBy: "foo", + }, + }, + }, + }; + testConfigMigration(otherConfig, otherConfig); + })); }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 4d0423df59..c9c0370e4a 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -72,6 +72,7 @@ export class ConfigService extends LatestEntityLoader { migratePhotoDatatype, migratePercentageDatatype, migrateEntityBlock, + migrateGroupByConfig, addDefaultNoteDetailsConfig, ]; @@ -395,3 +396,17 @@ const addDefaultNoteDetailsConfig: ConfigMigration = (key, configPart) => { return configPart; }; + +const migrateGroupByConfig: ConfigMigration = (key, configPart) => { + // Check if we are working with the EntityCountDashboard component and within the 'config' object + if ( + configPart?.component === "EntityCountDashboard" && + typeof configPart?.config?.groupBy === "string" + ) { + configPart.config.groupBy = [configPart.config.groupBy]; // Wrap groupBy as an array + return configPart; + } + + // Return the unchanged part if no modification is needed + return configPart; +}; diff --git a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts index 3fef780251..18e0bb32ab 100644 --- a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts +++ b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts @@ -9,6 +9,7 @@ import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic- import { EntityAbility } from "../../permissions/ability/entity-ability"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { SessionSubject } from "../../session/auth/session-info"; +import { EntityCountDashboardConfig } from "app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component"; describe("DashboardComponent", () => { let component: DashboardComponent; @@ -38,7 +39,10 @@ describe("DashboardComponent", () => { { component: "EntityCountDashboard" }, { component: "EntityCountDashboard", - config: { entity: "School", groupBy: "language" }, + config: { + entityType: "School", + groupBy: ["language"], + } as EntityCountDashboardConfig, }, { component: "ShortcutDashboard", config: { shortcuts: [] } }, ]; diff --git a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.html b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.html index ee36b34c96..fd7a67f9ef 100644 --- a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.html +++ b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.html @@ -3,8 +3,45 @@ theme="child" [title]="totalEntities" [subtitle]="label" - [entries]="entityGroupCounts" + [entries]="entityGroupCounts[groupBy[currentGroupIndex]]" > +
+ + + + by + + + + + +
+
@@ -46,7 +83,7 @@ { component = fixture.componentInstance; component.entityType = TestEntity.ENTITY_TYPE; - component.groupBy = "category"; + component.groupBy = ["category", "other", "ref"]; fixture.detectChanges(); }); @@ -62,16 +62,22 @@ describe("EntityCountDashboardComponent", () => { await component.ngOnInit(); - expect(component.entityGroupCounts) + const currentlyShownGroupCounts = + component.entityGroupCounts[ + component.groupBy[component.currentGroupIndex] + ]; + expect(currentlyShownGroupCounts.length) .withContext("unexpected number of centersWithProbability") - .toHaveSize(2); - const actualCenterAEntry = component.entityGroupCounts.filter( + .toBe(2); + + const actualCenterAEntry = currentlyShownGroupCounts.filter( (e) => e.label === centerA.label, )[0]; expect(actualCenterAEntry.value) .withContext("child count of CenterA not correct") .toBe(2); - const actualCenterBEntry = component.entityGroupCounts.filter( + + const actualCenterBEntry = currentlyShownGroupCounts.filter( (e) => e.label === centerB.label, )[0]; expect(actualCenterBEntry.value) @@ -82,7 +88,7 @@ describe("EntityCountDashboardComponent", () => { it("should groupBy enum values and display label", async () => { const testGroupBy = "test"; TestEntity.schema.set(testGroupBy, { dataType: "configurable-enum" }); - component.groupBy = testGroupBy; + component.groupBy = [testGroupBy]; const children = [ new TestEntity(), @@ -99,16 +105,23 @@ describe("EntityCountDashboardComponent", () => { await component.ngOnInit(); - expect(component.entityGroupCounts).toHaveSize(3); - expect(component.entityGroupCounts).toContain({ + const currentlyShownGroupCounts = + component.entityGroupCounts[ + component.groupBy[component.currentGroupIndex] + ]; + + expect(currentlyShownGroupCounts).toHaveSize(3); + expect(currentlyShownGroupCounts).toContain({ label: c1.label, value: 2, id: c1.id, + groupedByEntity: undefined, }); - expect(component.entityGroupCounts).toContain({ + expect(currentlyShownGroupCounts).toContain({ label: c2.label, value: 1, id: c2.id, + groupedByEntity: undefined, }); TestEntity.schema.delete(testGroupBy); @@ -116,7 +129,7 @@ describe("EntityCountDashboardComponent", () => { it("should groupBy entity references and display an entity-block", async () => { const testGroupBy = "ref"; - component.groupBy = testGroupBy; + component.groupBy = [testGroupBy]; component.entityType = TestEntity.ENTITY_TYPE; const c1 = new Entity("ref-1"); @@ -128,23 +141,29 @@ describe("EntityCountDashboardComponent", () => { await component.ngOnInit(); - expect(component.groupedByEntity).toBe(TestEntity.ENTITY_TYPE); - expect(component.entityGroupCounts).toHaveSize(2); - expect(component.entityGroupCounts).toContain({ + const currentlyShownGroupCounts = + component.entityGroupCounts[ + component.groupBy[component.currentGroupIndex] + ]; + + expect(currentlyShownGroupCounts).toHaveSize(2); + expect(currentlyShownGroupCounts).toContain({ label: "", value: 1, id: "", + groupedByEntity: TestEntity.ENTITY_TYPE, }); - expect(component.entityGroupCounts).toContain({ + expect(currentlyShownGroupCounts).toContain({ label: c1.getId(), value: 1, id: c1.getId(), + groupedByEntity: TestEntity.ENTITY_TYPE, }); }); it("should groupBy arrays, split and summarized for individual array elements", async () => { const testGroupBy = "children"; - component.groupBy = testGroupBy; + component.groupBy = [testGroupBy]; component.entityType = Note.ENTITY_TYPE; const x0 = new Note(); @@ -157,21 +176,29 @@ describe("EntityCountDashboardComponent", () => { await component.ngOnInit(); - expect(component.entityGroupCounts).toHaveSize(3); - expect(component.entityGroupCounts).toContain({ + const currentlyShownGroupCounts = + component.entityGroupCounts[ + component.groupBy[component.currentGroupIndex] + ]; + + expect(currentlyShownGroupCounts).toHaveSize(3); + expect(currentlyShownGroupCounts).toContain({ label: "", value: 1, id: "", + groupedByEntity: "Child", }); - expect(component.entityGroupCounts).toContain({ + expect(currentlyShownGroupCounts).toContain({ label: "link-1", value: 2, id: "link-1", + groupedByEntity: "Child", }); - expect(component.entityGroupCounts).toContain({ + expect(currentlyShownGroupCounts).toContain({ label: "link-2", value: 1, id: "link-2", + groupedByEntity: "Child", }); }); }); diff --git a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.ts b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.ts index 4b62714021..4e7bf4f6bc 100644 --- a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.ts +++ b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.component.ts @@ -18,10 +18,36 @@ import { DashboardWidget } from "../../../../core/dashboard/dashboard-widget/das import { EntityDatatype } from "../../../../core/basic-datatypes/entity/entity.datatype"; import { EntityBlockComponent } from "../../../../core/basic-datatypes/entity/entity-block/entity-block.component"; import { NgIf } from "@angular/common"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { MatIconButton } from "@angular/material/button"; +import { EntityFieldLabelComponent } from "../../../../core/common-components/entity-field-label/entity-field-label.component"; -interface EntityCountDashboardConfig { - entity?: string; - groupBy?: string; +/** + * Configuration (stored in Config document in the DB) for the dashboard widget. + */ +export interface EntityCountDashboardConfig { + entityType?: string; + groupBy?: string[]; +} + +/** + * Details of one row of disaggregated counts (e.g. for a specific category value) to be displayed. + */ +interface GroupCountRow { + label: string; + id: string; + + /** + * The count of entities part of this group + */ + value: number; + + /** + * if the groupBy field is an entity reference this holds the related entity type, + * so that the entity block will be displayed instead of an id string, + * otherwise undefined, to display simply the group label. + */ + groupedByEntity: string; } @DynamicComponent("ChildrenCountDashboard") @@ -37,6 +63,9 @@ interface EntityCountDashboardConfig { DashboardListWidgetComponent, EntityBlockComponent, NgIf, + MatTooltipModule, + MatIconButton, + EntityFieldLabelComponent, ], standalone: true, }) @@ -44,8 +73,17 @@ export class EntityCountDashboardComponent extends DashboardWidget implements EntityCountDashboardConfig, OnInit { + getPrev() { + this.currentGroupIndex = + (this.currentGroupIndex - 1 + this.groupBy.length) % this.groupBy.length; + } + + getNext() { + this.currentGroupIndex = (this.currentGroupIndex + 1) % this.groupBy.length; + } + static override getRequiredEntities(config: EntityCountDashboardConfig) { - return config?.entity || "Child"; + return config?.entityType || "Child"; } /** @@ -56,23 +94,30 @@ export class EntityCountDashboardComponent this._entity = this.entities.get(value); } - private _entity: EntityConstructor; + protected _entity: EntityConstructor; + /** - * The property of the Child entities to group counts by. + * The property of the entities to group counts by. * * Default is "center". */ - @Input() groupBy = "center"; + @Input() groupBy: string[] = ["center", "gender"]; /** - * if the groupBy field is an entity reference this holds the related entity type, - * so that the entity block will be displayed instead of an id string, - * otherwise undefined, to display simply the group label. - * */ - groupedByEntity: string; + * The counts of entities for each of the groupBy fields. + */ + entityGroupCounts: { [groupBy: string]: GroupCountRow[] } = {}; + + /** + * Index of the currently displayed groupBy field / entityGroupCounts entry. + */ + currentGroupIndex = 0; totalEntities: number; - entityGroupCounts: { label: string; value: number; id: string }[] = []; + + /** + * The label of the entity type (displayed as an overall dashboard widget subtitle) + */ label: string; entityIcon: IconName; @@ -88,38 +133,46 @@ export class EntityCountDashboardComponent if (!this._entity) { this.entityType = "Child"; } - - const groupByType = this._entity.schema.get(this.groupBy); - this.groupedByEntity = - groupByType.dataType === EntityDatatype.dataType - ? groupByType.additional - : undefined; - - const entities = await this.entityMapper.loadType(this._entity); - this.updateCounts(entities.filter((e) => e.isActive)); this.label = this._entity.labelPlural; this.entityIcon = this._entity.icon; - } - goToChildrenList(filterId: string) { - const params = {}; - params[this.groupBy] = filterId; - - this.router.navigate([this._entity.route], { queryParams: params }); + const entities = await this.entityMapper.loadType(this._entity); + this.totalEntities = entities.length; + for (const groupByField of this.groupBy) { + this.entityGroupCounts[groupByField] = this.calculateGroupCounts( + entities.filter((e) => e.isActive), + groupByField, + ); + } } - private updateCounts(entities: Entity[]) { - this.totalEntities = entities.length; - const groups = groupBy(entities, this.groupBy as keyof Entity); - this.entityGroupCounts = groups.map(([group, entities]) => { + private calculateGroupCounts( + entities: Entity[], + groupByField: string, + ): GroupCountRow[] { + const groupByType = this._entity.schema.get(groupByField); + const groups = groupBy(entities, groupByField as keyof Entity); + return groups.map(([group, entities]) => { const label = extractHumanReadableLabel(group); + const groupedByEntity = + groupByType.dataType === EntityDatatype.dataType + ? groupByType.additional + : undefined; return { label: label, value: entities.length, id: group?.["id"] || label, + groupedByEntity: groupedByEntity, }; }); } + + goToEntityList(filterId: string) { + const params = {}; + params[this.groupBy[this.currentGroupIndex]] = filterId; + + this.router.navigate([this._entity.route], { queryParams: params }); + } } /** diff --git a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.stories.ts b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.stories.ts index 5b62c13bc6..072a3e7a3a 100644 --- a/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.stories.ts +++ b/src/app/features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard/entity-count-dashboard.stories.ts @@ -13,11 +13,11 @@ export default { providers: [ importProvidersFrom( StorybookBaseModule.withData([ - TestEntity.create({ category: genders[0] }), - TestEntity.create({ category: genders[1] }), - TestEntity.create({ category: genders[1] }), - TestEntity.create({ category: genders[1] }), - TestEntity.create({ category: genders[2] }), + TestEntity.create({ category: genders[0], other: "otherA" }), + TestEntity.create({ category: genders[1], other: "otherB" }), + TestEntity.create({ category: genders[1], other: "otherC" }), + TestEntity.create({ category: genders[1], other: "otherD" }), + TestEntity.create({ category: genders[2], other: "otherE" }), ]), ), ], @@ -33,10 +33,8 @@ const Template: StoryFn = ( }); export const Primary = { - render: Template, - args: { entityType: "TestEntity", - groupBy: "category", + groupBy: ["category", "other"], }, };
- {{ group.label }} + {{ group.label }}