Skip to content

feat(#9231): enable place filter aggregate targets for users with multiple facility_ids #9232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions webapp/src/css/inbox.less
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,27 @@ body {
left: 0;
right: 0;
display: flex;
.right-side-filter {
margin-left: auto;
.open-filter-label {
color: @filter-text-color;
}
.btn.open-filter {
background-color: transparent;
margin: 0;
&:focus,
&:active {
outline: none;
border: none;
box-shadow: none;
}
.fa {
font-size: @font-extra-large;
color: @filter-icon-color;
padding: 2px 2px 0 0;
}
}
}
}

.freetext-filter {
Expand Down Expand Up @@ -1151,13 +1172,12 @@ a.fa:hover {
.analytics {
.content {
.inner {
/* allow the scrollable content to grow to full width */
max-width: inherit;
max-width: 100%;
.page {
max-width: 100%;
}
.page:not(.target-aggregates) {
max-width: inherit;
/* set the max with and center all the children again */
> div {
max-width: @content-width;
margin: 0 auto;
}
}
Expand All @@ -1166,8 +1186,16 @@ a.fa:hover {
/* position this absolute div in the horizontal center */
left: 0;
right: 0;
max-width: @content-width;
margin-left: auto;
margin-right: auto;

&.sidebar-closed {
margin-right: auto;
}

&.sidebar-open {
margin-right: 0;
}
}
}
}
Expand Down Expand Up @@ -1712,6 +1740,10 @@ mm-search-bar {
display: none;
}

.open-filter-label {
display: none;
}

mm-search-bar {
padding: 5px;
.mm-search-bar-container {
Expand All @@ -1730,10 +1762,6 @@ mm-search-bar {
.btn.open-filter {
margin: 0;

.open-filter-label {
display: none;
}

.filter-counter {
position: absolute;
top: 3px;
Expand Down Expand Up @@ -1779,6 +1807,9 @@ mm-search-bar {
.filters .filter {
width: 17%;
}
.right-side-filter {
padding-right: 6px;
}
.header {
.tabs {
padding-left: 2px;
Expand Down
62 changes: 62 additions & 0 deletions webapp/src/css/sidebar-filter.less
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,68 @@
overflow-y: auto;
overflow-x: hidden;

.filter-options-container {
padding-top: 25px;
padding-left: 18px;
padding-right: 18px;
width: 100%;
height: 100%;

.filter-options-title {
display: block;
height: 20px;
font-weight: bold;
font-size: @font-medium;
color: @gray-ultra-dark;
}

.filter-option-place {
padding-top: 20px;

.filter-option-wrapper {
display: flex;
align-items: center;
margin: 0;

.filter-option-radio {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
min-width: 20px;
min-height: 20px;
border: 3px solid @gray-medium-dark;
border-radius: 50%;
outline: none;
margin: 0;
position: relative;
}

.filter-option-radio:checked {
border-color: @blue-dark;
}

.filter-option-radio:checked::after {
content: '';
min-width: 7px;
min-height: 7px;
background: @blue-dark;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
}
}

.filter-option-label {
font-size: @font-medium;
font-weight: normal;
color: @gray-ultra-dark;
padding-left: 10px;
}
}
}

mat-expansion-panel {
margin: 0;
box-shadow: none;
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/css/variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
@chip-border-color: @white;
@sidebar-background-color: @white;
@filter-icon-color: @gray-ultra-dark;
@filter-text-color: @gray-medium;
@filter-text-color: @black;
@progress-bar-background-color: @white;
@targets-progress-bar-unmet-goal: @gray-medium-dark;
@backdrop-color: @black;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@
</ul>
</span>

<div class="right-side-filter" *ngIf="showFilterButton">
<button class="btn open-filter" (click)="openSidebar()">
<span class="fa fa-sliders"></span>
<span class="open-filter-label">{{ 'search_bar.filter.label' | translate }}</span>
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
import { AfterContentChecked, AfterContentInit, Component, Input, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import {
AfterContentChecked,
AfterContentInit,
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { Store } from '@ngrx/store';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Subscription, filter } from 'rxjs';

import { GlobalActions } from '@mm-actions/global';
import { Selectors } from '@mm-selectors/index';
import { AuthService } from '@mm-services/auth.service';
import { SessionService } from '@mm-services/session.service';
import { TelemetryService } from '@mm-services/telemetry.service';
import { TargetAggregatesService } from '@mm-services/target-aggregates.service';
import { UserSettingsService } from '@mm-services/user-settings.service';
import { OLD_REPORTS_FILTER_PERMISSION } from '@mm-modules/reports/reports-filters.component';
import { OLD_ACTION_BAR_PERMISSION } from '@mm-components/actionbar/actionbar.component';
import { AGGREGATE_TARGETS_ID } from '@mm-services/analytics-modules.service';

@Component({
selector: 'mm-analytics-filters',
templateUrl: './analytics-filter.component.html'
})
export class AnalyticsFilterComponent implements AfterContentInit, AfterContentChecked, OnDestroy {
export class AnalyticsFilterComponent implements AfterContentInit, AfterContentChecked, OnInit, OnDestroy {
@Input() analyticsModules: any[] = [];
@Output() toggleFilter: EventEmitter<any> = new EventEmitter();

private globalActions;
activeModule;
subscriptions: Subscription = new Subscription();
showFilterButton;
isOpen = false;

constructor(
private store: Store,
private route: ActivatedRoute,
) { }
private router: Router,
private authService: AuthService,
private sessionService: SessionService,
private telemetryService: TelemetryService,
private targetAggregatesService: TargetAggregatesService,
private userSettingsService: UserSettingsService
) {
this.globalActions = new GlobalActions(store);
}

ngOnInit() {
this.canDisplayFilterButton();
this.subscribeToRouteChanges();
this.subscribeToStore();
}

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

ngOnDestroy(): void {
this.subscriptions.unsubscribe();
this.globalActions.clearSidebarFilter();
}

private subscribeToStore() {
const subscription = this.store
.select(Selectors.getSidebarFilter)
.subscribe((filterState) => this.isOpen = filterState?.isOpen ?? false);
this.subscriptions.add(subscription);
}

private getCurrentModuleId() {
return this.route.snapshot?.firstChild?.data?.moduleId;
}

private setActiveModule() {
this.activeModule = this.analyticsModules?.find(module => {
return module.id === this.route.snapshot?.firstChild?.data?.moduleId;
});
const currentModuleId = this.getCurrentModuleId();
this.activeModule = this.analyticsModules?.find(module => module.id === currentModuleId);
}

private subscribeToRouteChanges() {
const routeSubscription = this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => this.canDisplayFilterButton());
this.subscriptions.add(routeSubscription);
}

private checkPermissions() {
const permissions = [
OLD_REPORTS_FILTER_PERMISSION,
OLD_ACTION_BAR_PERMISSION
];

return this.authService
.has(permissions)
.then((permissions) => permissions === false);
}

private isTargetAggregates() {
return this.getCurrentModuleId() === AGGREGATE_TARGETS_ID;
}

private isTargetAggregateEnabled() {
return this.targetAggregatesService.isEnabled();
}

private async canDisplayFilterButton() {
const isAdmin = this.sessionService.isAdmin();
const [hasMultipleFacilities, checkPermissions, isTargetAggregateEnabled] = await Promise.all([
this.userSettingsService.hasMultipleFacilities(),
this.checkPermissions(),
this.isTargetAggregateEnabled(),
]);

this.showFilterButton = !isAdmin &&
hasMultipleFacilities &&
checkPermissions &&
this.isTargetAggregates() &&
isTargetAggregateEnabled;
}

openSidebar() {
this.isOpen = !this.isOpen;
this.globalActions.setSidebarFilter({ isOpen: this.isOpen });
if (this.isOpen) {
// Counting every time the user opens the sidebar filter in analytics_targets_aggregrate tab.
this.telemetryService.record('sidebar_filter:analytics_target_aggregates:open');
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="content-pane right-pane">
<div class="col-sm-8 item-content empty-selection" *ngIf="!selected && !error">
<div>
<div class="item-content empty-selection" *ngIf="!selected && !error">
<div>{{'analytics.target.aggregates.no.target.selected' | translate}}</div>
</div>

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

<div class="col-sm-8 item-content" *ngIf="selected && !selected.error && !error">
<div *ngIf="selected && !selected.error && !error">
<div class="material">
<div class="body meta">
<div class="target-detail card" [ngClass]="{ 'has-goal': selected.goal >= 0, 'goal-met': (selected.value?.pass >= selected.goal) || (selected.value?.percent >= selected.goal) }">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<section class="sidebar-filter">
<div
class="sidebar-backdrop"
[ngClass]="{ hidden: !isOpen }"
(click)="toggleSidebarFilter()"
(keydown.escape)="toggleSidebarFilter()">
</div>

<div class="sidebar-main" [ngClass]="{ hidden: !isOpen }">
<div class="sidebar-header">
<p class="sidebar-title">{{ 'reports.sidebar.filter.title' | translate }}</p>
<a class="sidebar-close fa fa-times" (click)="toggleSidebarFilter()"></a>
</div>

<div class="sidebar-body">
<mat-accordion multi>
<div class="filter-options-container">
<span class="filter-options-title"> {{ facilityFilterLabel | translate }} </span>
<ng-container *ngFor="let facility of userFacilities">
<div class="filter-option-place">
<label class="filter-option-wrapper">
<input
type="radio"
name="facility"
[value]="facility._id"
class="filter-option-radio"
[(ngModel)]="selectedFacilityId">
<span class="filter-option-label">{{ facility.name }}</span>
</label>
</div>
</ng-container>
</div>
</mat-accordion>
</div>

<div class="sidebar-footer">
<button type="button" class="btn btn-primary" (click)="toggleSidebarFilter()">{{ 'reports.sidebar.filter.submit' | translate }}</button>
</div>
</div>
</section>
Loading
Loading