Skip to content

Commit ac58750

Browse files
committed
feat(#9231): enable place filter aggregate targets for users with multiple facility_ids (#9232)
1 parent 3c2b140 commit ac58750

21 files changed

+968
-36
lines changed

webapp/src/css/inbox.less

+41-10
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,27 @@ body {
406406
left: 0;
407407
right: 0;
408408
display: flex;
409+
.right-side-filter {
410+
margin-left: auto;
411+
.open-filter-label {
412+
color: @filter-text-color;
413+
}
414+
.btn.open-filter {
415+
background-color: transparent;
416+
margin: 0;
417+
&:focus,
418+
&:active {
419+
outline: none;
420+
border: none;
421+
box-shadow: none;
422+
}
423+
.fa {
424+
font-size: @font-extra-large;
425+
color: @filter-icon-color;
426+
padding: 2px 2px 0 0;
427+
}
428+
}
429+
}
409430
}
410431

411432
.freetext-filter {
@@ -1151,13 +1172,12 @@ a.fa:hover {
11511172
.analytics {
11521173
.content {
11531174
.inner {
1154-
/* allow the scrollable content to grow to full width */
1155-
max-width: inherit;
1175+
max-width: 100%;
1176+
.page {
1177+
max-width: 100%;
1178+
}
11561179
.page:not(.target-aggregates) {
1157-
max-width: inherit;
1158-
/* set the max with and center all the children again */
11591180
> div {
1160-
max-width: @content-width;
11611181
margin: 0 auto;
11621182
}
11631183
}
@@ -1166,8 +1186,16 @@ a.fa:hover {
11661186
/* position this absolute div in the horizontal center */
11671187
left: 0;
11681188
right: 0;
1189+
max-width: @content-width;
11691190
margin-left: auto;
1170-
margin-right: auto;
1191+
1192+
&.sidebar-closed {
1193+
margin-right: auto;
1194+
}
1195+
1196+
&.sidebar-open {
1197+
margin-right: 0;
1198+
}
11711199
}
11721200
}
11731201
}
@@ -1712,6 +1740,10 @@ mm-search-bar {
17121740
display: none;
17131741
}
17141742

1743+
.open-filter-label {
1744+
display: none;
1745+
}
1746+
17151747
mm-search-bar {
17161748
padding: 5px;
17171749
.mm-search-bar-container {
@@ -1730,10 +1762,6 @@ mm-search-bar {
17301762
.btn.open-filter {
17311763
margin: 0;
17321764

1733-
.open-filter-label {
1734-
display: none;
1735-
}
1736-
17371765
.filter-counter {
17381766
position: absolute;
17391767
top: 3px;
@@ -1779,6 +1807,9 @@ mm-search-bar {
17791807
.filters .filter {
17801808
width: 17%;
17811809
}
1810+
.right-side-filter {
1811+
padding-right: 6px;
1812+
}
17821813
.header {
17831814
.tabs {
17841815
padding-left: 2px;

webapp/src/css/sidebar-filter.less

+62
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,68 @@
9999
overflow-y: auto;
100100
overflow-x: hidden;
101101

102+
.filter-options-container {
103+
padding-top: 25px;
104+
padding-left: 18px;
105+
padding-right: 18px;
106+
width: 100%;
107+
height: 100%;
108+
109+
.filter-options-title {
110+
display: block;
111+
height: 20px;
112+
font-weight: bold;
113+
font-size: @font-medium;
114+
color: @gray-ultra-dark;
115+
}
116+
117+
.filter-option-place {
118+
padding-top: 20px;
119+
120+
.filter-option-wrapper {
121+
display: flex;
122+
align-items: center;
123+
margin: 0;
124+
125+
.filter-option-radio {
126+
-webkit-appearance: none;
127+
-moz-appearance: none;
128+
appearance: none;
129+
min-width: 20px;
130+
min-height: 20px;
131+
border: 3px solid @gray-medium-dark;
132+
border-radius: 50%;
133+
outline: none;
134+
margin: 0;
135+
position: relative;
136+
}
137+
138+
.filter-option-radio:checked {
139+
border-color: @blue-dark;
140+
}
141+
142+
.filter-option-radio:checked::after {
143+
content: '';
144+
min-width: 7px;
145+
min-height: 7px;
146+
background: @blue-dark;
147+
position: absolute;
148+
top: 50%;
149+
left: 50%;
150+
transform: translate(-50%, -50%);
151+
border-radius: 50%;
152+
}
153+
}
154+
155+
.filter-option-label {
156+
font-size: @font-medium;
157+
font-weight: normal;
158+
color: @gray-ultra-dark;
159+
padding-left: 10px;
160+
}
161+
}
162+
}
163+
102164
mat-expansion-panel {
103165
margin: 0;
104166
box-shadow: none;

webapp/src/css/variables.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
@chip-border-color: @white;
4242
@sidebar-background-color: @white;
4343
@filter-icon-color: @gray-ultra-dark;
44-
@filter-text-color: @gray-medium;
44+
@filter-text-color: @black;
4545
@progress-bar-background-color: @white;
4646
@targets-progress-bar-unmet-goal: @gray-medium-dark;
4747
@backdrop-color: @black;

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

+6
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@
2020
</ul>
2121
</span>
2222

23+
<div class="right-side-filter" *ngIf="showFilterButton">
24+
<button class="btn open-filter" (click)="openSidebar()">
25+
<span class="fa fa-sliders"></span>
26+
<span class="open-filter-label">{{ 'search_bar.filter.label' | translate }}</span>
27+
</button>
28+
</div>
2329
</div>
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,60 @@
1-
import { AfterContentChecked, AfterContentInit, Component, Input, OnDestroy } from '@angular/core';
2-
import { ActivatedRoute } from '@angular/router';
3-
import { Subscription } from 'rxjs';
1+
import {
2+
AfterContentChecked,
3+
AfterContentInit,
4+
Component,
5+
EventEmitter,
6+
Input,
7+
OnDestroy,
8+
OnInit,
9+
Output
10+
} from '@angular/core';
11+
import { Store } from '@ngrx/store';
12+
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
13+
import { Subscription, filter } from 'rxjs';
14+
15+
import { GlobalActions } from '@mm-actions/global';
16+
import { Selectors } from '@mm-selectors/index';
17+
import { AuthService } from '@mm-services/auth.service';
18+
import { SessionService } from '@mm-services/session.service';
19+
import { TelemetryService } from '@mm-services/telemetry.service';
20+
import { TargetAggregatesService } from '@mm-services/target-aggregates.service';
21+
import { UserSettingsService } from '@mm-services/user-settings.service';
22+
import { OLD_REPORTS_FILTER_PERMISSION } from '@mm-modules/reports/reports-filters.component';
23+
import { OLD_ACTION_BAR_PERMISSION } from '@mm-components/actionbar/actionbar.component';
24+
import { AGGREGATE_TARGETS_ID } from '@mm-services/analytics-modules.service';
425

526
@Component({
627
selector: 'mm-analytics-filters',
728
templateUrl: './analytics-filter.component.html'
829
})
9-
export class AnalyticsFilterComponent implements AfterContentInit, AfterContentChecked, OnDestroy {
30+
export class AnalyticsFilterComponent implements AfterContentInit, AfterContentChecked, OnInit, OnDestroy {
1031
@Input() analyticsModules: any[] = [];
32+
@Output() toggleFilter: EventEmitter<any> = new EventEmitter();
33+
34+
private globalActions;
1135
activeModule;
1236
subscriptions: Subscription = new Subscription();
37+
showFilterButton;
38+
isOpen = false;
1339

1440
constructor(
41+
private store: Store,
1542
private route: ActivatedRoute,
16-
) { }
43+
private router: Router,
44+
private authService: AuthService,
45+
private sessionService: SessionService,
46+
private telemetryService: TelemetryService,
47+
private targetAggregatesService: TargetAggregatesService,
48+
private userSettingsService: UserSettingsService
49+
) {
50+
this.globalActions = new GlobalActions(store);
51+
}
52+
53+
ngOnInit() {
54+
this.canDisplayFilterButton();
55+
this.subscribeToRouteChanges();
56+
this.subscribeToStore();
57+
}
1758

1859
ngAfterContentInit() {
1960
const subscription = this.route.url.subscribe(() => this.setActiveModule());
@@ -28,11 +69,72 @@ export class AnalyticsFilterComponent implements AfterContentInit, AfterContentC
2869

2970
ngOnDestroy(): void {
3071
this.subscriptions.unsubscribe();
72+
this.globalActions.clearSidebarFilter();
73+
}
74+
75+
private subscribeToStore() {
76+
const subscription = this.store
77+
.select(Selectors.getSidebarFilter)
78+
.subscribe((filterState) => this.isOpen = filterState?.isOpen ?? false);
79+
this.subscriptions.add(subscription);
80+
}
81+
82+
private getCurrentModuleId() {
83+
return this.route.snapshot?.firstChild?.data?.moduleId;
3184
}
3285

3386
private setActiveModule() {
34-
this.activeModule = this.analyticsModules?.find(module => {
35-
return module.id === this.route.snapshot?.firstChild?.data?.moduleId;
36-
});
87+
const currentModuleId = this.getCurrentModuleId();
88+
this.activeModule = this.analyticsModules?.find(module => module.id === currentModuleId);
89+
}
90+
91+
private subscribeToRouteChanges() {
92+
const routeSubscription = this.router.events
93+
.pipe(filter(event => event instanceof NavigationEnd))
94+
.subscribe(() => this.canDisplayFilterButton());
95+
this.subscriptions.add(routeSubscription);
96+
}
97+
98+
private checkPermissions() {
99+
const permissions = [
100+
OLD_REPORTS_FILTER_PERMISSION,
101+
OLD_ACTION_BAR_PERMISSION
102+
];
103+
104+
return this.authService
105+
.has(permissions)
106+
.then((permissions) => permissions === false);
107+
}
108+
109+
private isTargetAggregates() {
110+
return this.getCurrentModuleId() === AGGREGATE_TARGETS_ID;
111+
}
112+
113+
private isTargetAggregateEnabled() {
114+
return this.targetAggregatesService.isEnabled();
115+
}
116+
117+
private async canDisplayFilterButton() {
118+
const isAdmin = this.sessionService.isAdmin();
119+
const [hasMultipleFacilities, checkPermissions, isTargetAggregateEnabled] = await Promise.all([
120+
this.userSettingsService.hasMultipleFacilities(),
121+
this.checkPermissions(),
122+
this.isTargetAggregateEnabled(),
123+
]);
124+
125+
this.showFilterButton = !isAdmin &&
126+
hasMultipleFacilities &&
127+
checkPermissions &&
128+
this.isTargetAggregates() &&
129+
isTargetAggregateEnabled;
130+
}
131+
132+
openSidebar() {
133+
this.isOpen = !this.isOpen;
134+
this.globalActions.setSidebarFilter({ isOpen: this.isOpen });
135+
if (this.isOpen) {
136+
// Counting every time the user opens the sidebar filter in analytics_targets_aggregrate tab.
137+
this.telemetryService.record('sidebar_filter:analytics_target_aggregates:open');
138+
}
37139
}
38140
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
<div class="content-pane right-pane">
2-
<div class="col-sm-8 item-content empty-selection" *ngIf="!selected && !error">
1+
<div>
2+
<div class="item-content empty-selection" *ngIf="!selected && !error">
33
<div>{{'analytics.target.aggregates.no.target.selected' | translate}}</div>
44
</div>
55

6-
<div class="col-sm-8 item-content empty-selection selection-error" *ngIf="error || selected?.error">
6+
<div class="item-content empty-selection selection-error" *ngIf="error || selected?.error">
77
<div>{{ (error?.translationKey || selected?.error?.translationKey || 'analytics.target.aggregates.select.error') | translate}}</div>
88
</div>
99

10-
<div class="col-sm-8 item-content" *ngIf="selected && !selected.error && !error">
10+
<div *ngIf="selected && !selected.error && !error">
1111
<div class="material">
1212
<div class="body meta">
1313
<div class="target-detail card" [ngClass]="{ 'has-goal': selected.goal >= 0, 'goal-met': (selected.value?.pass >= selected.goal) || (selected.value?.percent >= selected.goal) }">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<section class="sidebar-filter">
2+
<div
3+
class="sidebar-backdrop"
4+
[ngClass]="{ hidden: !isOpen }"
5+
(click)="toggleSidebarFilter()"
6+
(keydown.escape)="toggleSidebarFilter()">
7+
</div>
8+
9+
<div class="sidebar-main" [ngClass]="{ hidden: !isOpen }">
10+
<div class="sidebar-header">
11+
<p class="sidebar-title">{{ 'reports.sidebar.filter.title' | translate }}</p>
12+
<a class="sidebar-close fa fa-times" (click)="toggleSidebarFilter()"></a>
13+
</div>
14+
15+
<div class="sidebar-body">
16+
<mat-accordion multi>
17+
<div class="filter-options-container">
18+
<span class="filter-options-title"> {{ facilityFilterLabel | translate }} </span>
19+
<ng-container *ngFor="let facility of userFacilities">
20+
<div class="filter-option-place">
21+
<label class="filter-option-wrapper">
22+
<input
23+
type="radio"
24+
name="facility"
25+
[value]="facility._id"
26+
class="filter-option-radio"
27+
[(ngModel)]="selectedFacilityId">
28+
<span class="filter-option-label">{{ facility.name }}</span>
29+
</label>
30+
</div>
31+
</ng-container>
32+
</div>
33+
</mat-accordion>
34+
</div>
35+
36+
<div class="sidebar-footer">
37+
<button type="button" class="btn btn-primary" (click)="toggleSidebarFilter()">{{ 'reports.sidebar.filter.submit' | translate }}</button>
38+
</div>
39+
</div>
40+
</section>

0 commit comments

Comments
 (0)