Skip to content

Commit fcac9a2

Browse files
committed
chore(#9231): filter target aggregate data using sidebar filter (#9267)
1 parent 581f39d commit fcac9a2

12 files changed

+183
-19
lines changed

webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export class AnalyticsFilterComponent implements AfterContentInit, AfterContentC
139139
this.isOpen = !this.isOpen;
140140
this.globalActions.setSidebarFilter({ isOpen: this.isOpen });
141141
if (this.isOpen) {
142+
// Counting every time the user opens the sidebar filter in analytics_targets_aggregrate tab.
142143
this.telemetryService.record('sidebar_filter:analytics:target_aggregates:open');
143144
}
144145
}

webapp/src/ts/modules/analytics/analytics-target-aggregates-detail.component.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
22
import { ActivatedRoute } from '@angular/router';
33
import { combineLatest, Subscription, Subject } from 'rxjs';
44
import { Store } from '@ngrx/store';
5+
import { isEqual } from 'lodash-es';
56

67
import { Selectors } from '@mm-selectors/index';
78
import { TargetAggregatesActions } from '@mm-actions/target-aggregates';
@@ -19,6 +20,8 @@ export class AnalyticsTargetAggregatesDetailComponent implements OnInit, OnDestr
1920
subscriptions: Subscription = new Subscription();
2021
selected: any = null;
2122
error: any = null;
23+
private prevAggregateId;
24+
private prevAggregates;
2225
private aggregates = null;
2326
private viewInited = new Subject();
2427

@@ -64,12 +67,19 @@ export class AnalyticsTargetAggregatesDetailComponent implements OnInit, OnDestr
6467
this.route.params,
6568
this.viewInited,
6669
this.store.select(Selectors.getTargetAggregatesLoaded),
67-
).subscribe(([params, inited, loaded]) => {
70+
this.store.select(Selectors.getTargetAggregates),
71+
).subscribe(([params, inited, loaded, aggregates]) => {
6872
if (loaded && inited && params) {
6973
setTimeout(() => {
7074
// two birds with one stone
7175
// both this component and the parent (analytics-target-aggregates) need to be updated
72-
this.getAggregatesDetail(params.id);
76+
const aggregatesChanged = !isEqual(this.prevAggregates, aggregates);
77+
78+
if (params.id !== this.prevAggregateId || aggregatesChanged) {
79+
this.prevAggregateId = params.id;
80+
this.prevAggregates = aggregates;
81+
this.getAggregatesDetail(params.id);
82+
}
7383
});
7484
}
7585
});

webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
name="facility"
2525
[value]="facility._id"
2626
class="filter-option-radio"
27-
[(ngModel)]="selectedFacilityId">
27+
[(ngModel)]="selectedFacilityId"
28+
(change)="fetchAggregateTargets(facility._id)">
2829
<span class="filter-option-label">{{ facility.name }}</span>
2930
</label>
3031
</div>

webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnDestroy, OnInit } from '@angular/core';
1+
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
22
import { Subscription } from 'rxjs';
33
import { Store } from '@ngrx/store';
44

@@ -14,6 +14,7 @@ import { UserSettingsService } from '@mm-services/user-settings.service';
1414
})
1515
export class AnalyticsTargetAggregatesSidebarFilterComponent implements OnInit, OnDestroy {
1616

17+
@Output() facilitySelected = new EventEmitter<string>();
1718
private globalActions;
1819
subscriptions: Subscription = new Subscription();
1920
error;
@@ -44,6 +45,7 @@ export class AnalyticsTargetAggregatesSidebarFilterComponent implements OnInit,
4445
}
4546

4647
ngOnDestroy() {
48+
this.globalActions.clearSidebarFilter();
4749
this.subscriptions.unsubscribe();
4850
}
4951

@@ -91,4 +93,8 @@ export class AnalyticsTargetAggregatesSidebarFilterComponent implements OnInit,
9193
this.facilityFilterLabel = FACILITY;
9294
}
9395
}
96+
97+
fetchAggregateTargets(facilityId) {
98+
this.facilitySelected.emit(facilityId);
99+
}
94100
}

webapp/src/ts/modules/analytics/analytics-target-aggregates.component.html

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
<div class="item-content" [ngClass]="{ 'col-sm-6': isSidebarFilterOpen, 'col-sm-8': !isSidebarFilterOpen }">
3131
<router-outlet></router-outlet>
3232
</div>
33-
<mm-analytics-target-aggregates-sidebar-filter class="col-sm-3 sidebar-filter-wrapper" *ngIf="useSidebarFilter">
33+
<mm-analytics-target-aggregates-sidebar-filter
34+
class="col-sm-3 sidebar-filter-wrapper"
35+
*ngIf="useSidebarFilter"
36+
(facilitySelected)="getTargetAggregates($event)">
3437
</mm-analytics-target-aggregates-sidebar-filter>
3538
</div>
3639
</div>

webapp/src/ts/modules/analytics/analytics-target-aggregates.component.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
22
import { Store } from '@ngrx/store';
33
import { combineLatest, Subscription } from 'rxjs';
44

5+
import { Selectors } from '@mm-selectors/index';
56
import { TargetAggregatesActions } from '@mm-actions/target-aggregates';
67
import { TargetAggregatesService } from '@mm-services/target-aggregates.service';
7-
import { Selectors } from '@mm-selectors/index';
88
import { PerformanceService } from '@mm-services/performance.service';
99
import { AnalyticsTargetAggregatesSidebarFilterComponent }
1010
from './analytics-target-aggregates-sidebar-filter.component';
@@ -36,7 +36,7 @@ export class AnalyticsTargetAggregatesComponent implements OnInit, OnDestroy {
3636
this.targetAggregatesActions = new TargetAggregatesActions(store);
3737
}
3838

39-
ngOnInit(): void {
39+
async ngOnInit(): Promise<void> {
4040
this.trackPerformance = this.performanceService.track();
4141
this.subscribeToStore();
4242
this.subscribeSidebarFilter();
@@ -68,7 +68,7 @@ export class AnalyticsTargetAggregatesComponent implements OnInit, OnDestroy {
6868
this.subscriptions.add(selectorsSubscription);
6969
}
7070

71-
private getTargetAggregates() {
71+
getTargetAggregates(userFacilityId?) {
7272
return this.targetAggregatesService
7373
.isEnabled()
7474
.then(enabled => {
@@ -78,7 +78,7 @@ export class AnalyticsTargetAggregatesComponent implements OnInit, OnDestroy {
7878
return;
7979
}
8080

81-
return this.targetAggregatesService.getAggregates();
81+
return this.targetAggregatesService.getAggregates(userFacilityId);
8282
})
8383
.then(aggregates => {
8484
this.targetAggregatesActions.setTargetAggregates(aggregates);

webapp/src/ts/services/target-aggregates.service.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -236,20 +236,25 @@ export class TargetAggregatesService {
236236
return Array.isArray(facility_id) ? facility_id : [ facility_id ];
237237
}
238238

239-
private async getHomePlace() {
239+
private async getHomePlace(facilityId?) {
240+
if (facilityId) {
241+
const places = await this.getDataRecordsService.get([facilityId]);
242+
return places?.[0];
243+
}
244+
240245
const facilityIds = await this.getUserFacilityIds();
241246
if (!facilityIds?.length) {
242247
return;
243248
}
244249
const places = await this.getDataRecordsService.get(facilityIds);
245-
return places?.length ? places[0] : undefined;
250+
return places?.[0];
246251
}
247252

248-
private getSupervisedContacts() {
253+
private getSupervisedContacts(facilityId?) {
249254
const alphabeticalSort = (a, b) => String(a.name).localeCompare(String(b.name));
250255

251256
return this
252-
.getHomePlace()
257+
.getHomePlace(facilityId)
253258
.then(homePlaceSummary => {
254259
if (!homePlaceSummary) {
255260
const message = 'Your user does not have an associated contact, or does not have access to the ' +
@@ -289,11 +294,11 @@ export class TargetAggregatesService {
289294
return !facilityIds || facilityIds.length > 0;
290295
}
291296

292-
getAggregates() {
293-
return this.ngZone.runOutsideAngular(() => this._getAggregates());
297+
getAggregates(facilityId?) {
298+
return this.ngZone.runOutsideAngular(() => this._getAggregates(facilityId));
294299
}
295300

296-
private _getAggregates() {
301+
private _getAggregates(facilityId?) {
297302
return this.settingsService
298303
.get()
299304
.then(settings => {
@@ -305,7 +310,7 @@ export class TargetAggregatesService {
305310

306311
return Promise
307312
.all([
308-
this.getSupervisedContacts(),
313+
this.getSupervisedContacts(facilityId),
309314
this.fetchLatestTargetDocs(settings)
310315
])
311316
.then(([ contacts, latestTargetDocs ]) => this.aggregateTargets(latestTargetDocs, contacts, targetsConfig));

webapp/src/ts/services/user-settings.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class UserSettingsService {
7171
});
7272
}
7373

74-
async getUserFacility(): Promise<string[]> {
74+
async getUserFacility() {
7575
return this
7676
.get()
7777
.then((userSettings: UserSettings) => {

webapp/tests/karma/ts/modules/analytics/analytics-target-aggregates-detail.component.spec.ts

+59-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angul
22
import { RouterTestingModule } from '@angular/router/testing';
33
import { ActivatedRoute } from '@angular/router';
44
import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
5-
import { provideMockStore } from '@ngrx/store/testing';
5+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
66
import { Subject } from 'rxjs';
77
import sinon from 'sinon';
88
import { expect } from 'chai';
@@ -23,6 +23,7 @@ describe('AnalyticsTargetAggregatesDetailComponent', () => {
2323
let targetAggregatesActions;
2424
let globalActions;
2525
let route;
26+
let store;
2627

2728
beforeEach(waitForAsync(() => {
2829
targetAggregatesService = {
@@ -64,10 +65,12 @@ describe('AnalyticsTargetAggregatesDetailComponent', () => {
6465
component = fixture.componentInstance;
6566
translateService = TestBed.inject(TranslateService);
6667
fixture.detectChanges();
68+
store = TestBed.inject(MockStore);
6769
});
6870
}));
6971

7072
afterEach(() => {
73+
store.resetSelectors();
7174
sinon.restore();
7275
});
7376

@@ -144,4 +147,59 @@ describe('AnalyticsTargetAggregatesDetailComponent', () => {
144147
expect(translateService.instant.callCount).to.equal(1);
145148
expect(translateService.instant.args[0]).to.deep.equal(['analytics.target.aggregates', undefined]);
146149
}));
150+
151+
it('should update title when aggregrates change', fakeAsync(() => {
152+
sinon.reset();
153+
targetAggregatesService.getAggregateDetails.returns({
154+
an: 'aggregate',
155+
translation_key: 'the_title',
156+
heading: 'the translated title'
157+
});
158+
translateService.instant = sinon.stub().returns('target aggregate');
159+
160+
route.params.next({ id: 'target1' });
161+
tick();
162+
163+
expect(targetAggregatesService.getAggregateDetails.callCount).to.equal(1);
164+
expect(targetAggregatesService.getAggregateDetails.args[0]).to.deep.equal(['target1', ['aggregates']]);
165+
expect(globalActions.setShowContent.callCount).to.equal(1);
166+
expect(globalActions.setShowContent.args[0][0]).to.equal(true);
167+
expect(globalActions.setTitle.callCount).to.equal(1);
168+
expect(globalActions.setTitle.args[0][0]).to.equal('target aggregate');
169+
expect(targetAggregatesActions.setSelectedTargetAggregate.callCount).to.equal(1);
170+
expect(targetAggregatesActions.setSelectedTargetAggregate.args[0][0]).to.deep.equal({
171+
an: 'aggregate',
172+
translation_key: 'the_title',
173+
heading: 'the translated title',
174+
});
175+
expect(translateService.instant.callCount).to.equal(1);
176+
expect(translateService.instant.args[0]).to.deep.equal(['analytics.target.aggregates', undefined]);
177+
178+
// Selecting a different user's facility which triggers changes to the aggregates
179+
const newAggregates = ['new aggregate 1', 'new aggregate 2'];
180+
store.overrideSelector(Selectors.getTargetAggregates, newAggregates);
181+
store.refreshState();
182+
183+
targetAggregatesService.getAggregateDetails.returns({
184+
an: 'aggregate1_updated',
185+
translation_key: 'title1_updated',
186+
heading: 'translated title 1 updated'
187+
});
188+
translateService.instant = sinon.stub().returns('target aggregate 1 updated');
189+
190+
tick();
191+
192+
expect(targetAggregatesService.getAggregateDetails.callCount).to.equal(2);
193+
expect(targetAggregatesService.getAggregateDetails.args[1]).to.deep.equal(['target1', newAggregates]);
194+
expect(globalActions.setShowContent.callCount).to.equal(2);
195+
expect(globalActions.setShowContent.args[1][0]).to.equal(true);
196+
expect(globalActions.setTitle.callCount).to.equal(2);
197+
expect(globalActions.setTitle.args[1][0]).to.equal('target aggregate 1 updated');
198+
expect(targetAggregatesActions.setSelectedTargetAggregate.callCount).to.equal(2);
199+
expect(targetAggregatesActions.setSelectedTargetAggregate.args[1][0]).to.deep.equal({
200+
an: 'aggregate1_updated',
201+
translation_key: 'title1_updated',
202+
heading: 'translated title 1 updated',
203+
});
204+
}));
147205
});

webapp/tests/karma/ts/modules/analytics/analytics-target-aggregates-sidebar-filter-component.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('Analytics Target Aggregate Sidebar Filter Component', () => {
3535
};
3636
globalActions = {
3737
setSidebarFilter: sinon.stub(GlobalActions.prototype, 'setSidebarFilter'),
38+
clearSidebarFilter: sinon.stub(GlobalActions.prototype, 'clearSidebarFilter'),
3839
};
3940

4041
const mockedSelectors = [
@@ -77,6 +78,12 @@ describe('Analytics Target Aggregate Sidebar Filter Component', () => {
7778
expect(component.error).to.be.undefined;
7879
}));
7980

81+
it('should clear sidebar filter in the store on component destroy', () => {
82+
component.ngOnDestroy();
83+
84+
expect(globalActions.clearSidebarFilter.calledOnce).to.be.true;
85+
});
86+
8087
it('should toggle sidebar filter', () => {
8188
component.toggleSidebarFilter();
8289
component.toggleSidebarFilter();

webapp/tests/karma/ts/modules/analytics/analytics-target-aggregates.component.spec.ts

+34
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,39 @@ describe('Analytics Target Aggregates Component', () => {
148148
expect(targetAggregatesActions.setTargetAggregatesError.callCount).to.equal(0);
149149
expect(targetAggregatesActions.setTargetAggregates.callCount).to.equal(1);
150150
expect(targetAggregatesActions.setTargetAggregates.args[0][0]).to.deep.equal(['some aggregates']);
151+
expect(targetAggregatesService.getAggregates.callCount).to.equal(1);
152+
}));
153+
154+
it('should set different aggregates when updateAggregateTargets is called with a new facility', fakeAsync(() => {
155+
sinon.reset();
156+
targetAggregatesService.isEnabled.resolves(true);
157+
targetAggregatesService.getAggregates.resolves(['some aggregates']);
158+
159+
component.ngOnInit();
160+
tick();
161+
162+
expect(targetAggregatesService.isEnabled.callCount).to.equal(1);
163+
expect(targetAggregatesService.getAggregates.callCount).to.equal(1);
164+
expect(component.loading).to.equal(false);
165+
expect(component.enabled).to.equal(true);
166+
expect(targetAggregatesActions.setTargetAggregatesError.callCount).to.equal(0);
167+
expect(targetAggregatesActions.setTargetAggregates.callCount).to.equal(1);
168+
expect(targetAggregatesActions.setTargetAggregates.args[0][0]).to.deep.equal(['some aggregates']);
169+
expect(targetAggregatesService.getAggregates.callCount).to.equal(1);
170+
171+
targetAggregatesActions.setTargetAggregates.resetHistory();
172+
targetAggregatesService.getAggregates.resetHistory();
173+
174+
const facilityTwoAggregates = ['new aggregates'];
175+
targetAggregatesService.getAggregates.withArgs('facility_2').resolves(facilityTwoAggregates);
176+
177+
// Fetch aggregates for user's second facility
178+
component.getTargetAggregates('facility_2');
179+
tick();
180+
181+
expect(targetAggregatesService.getAggregates.callCount).to.equal(1);
182+
expect(targetAggregatesService.getAggregates.calledWith('facility_2')).to.be.true;
183+
expect(targetAggregatesActions.setTargetAggregates.callCount).to.equal(1);
184+
expect(targetAggregatesActions.setTargetAggregates.args[0][0]).to.deep.equal(facilityTwoAggregates);
151185
}));
152186
});

webapp/tests/karma/ts/services/target-aggregates.service.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,45 @@ describe('TargetAggregatesService', () => {
488488
expect(getDataRecordsService.get.args[0]).to.deep.equal([[ 'home' ]]);
489489
});
490490

491+
it('should call getAggregates with provided facilityId when fetching aggregates', async () => {
492+
const facilityId = 'facility123';
493+
const config = { tasks: { targets: { items: [{ id: 'target', aggregate: true, type: 'count' }] } } };
494+
settingsService.get.resolves(config);
495+
496+
userSettingsService.get.resolves({ facility_id: 'home' });
497+
getDataRecordsService.get.resolves([]);
498+
getDataRecordsService.get.withArgs([facilityId]).resolves([{ _id: facilityId, name: 'Test Facility' }]);
499+
contactTypesService.getTypeId.returns('district');
500+
contactTypesService.getChildren.resolves([{ id: 'health_center' }]);
501+
searchService.search.resolves([]);
502+
503+
dbService.allDocs.resolves({ rows: [] });
504+
505+
uhcSettingsService.getMonthStartDate.returns(12);
506+
calendarIntervalService.getCurrent.returns({
507+
start: moment('2019-05-12').valueOf(),
508+
end: moment('2019-06-11').valueOf(),
509+
});
510+
511+
const result = await service.getAggregates(facilityId);
512+
513+
expect(result.length).to.equal(1);
514+
expect(result[0].id).to.equal('target');
515+
516+
expect(getDataRecordsService.get.callCount).to.equal(2);
517+
expect(getDataRecordsService.get.args[0]).to.deep.equal([[facilityId]]);
518+
expect(dbService.allDocs.callCount).to.equal(1);
519+
expect(dbService.allDocs.args[0]).to.deep.equal([{
520+
start_key: 'target~2019-06~',
521+
end_key: 'target~2019-06~\ufff0',
522+
include_docs: true
523+
}]);
524+
expect(uhcSettingsService.getMonthStartDate.callCount).to.equal(1);
525+
expect(uhcSettingsService.getMonthStartDate.args[0]).to.deep.equal([config]);
526+
expect(calendarIntervalService.getCurrent.callCount).to.equal(1);
527+
expect(calendarIntervalService.getCurrent.args[0]).to.deep.equal([12]);
528+
});
529+
491530
it('should fetch correct latest target docs', async () => {
492531
const config = { tasks: { targets: { items: [{ id: 'target', aggregate: true, type: 'count' }] } }};
493532
settingsService.get.resolves(config);

0 commit comments

Comments
 (0)