fix: multiple bugs in home/core/institution modules (#1225)
Fixes: #1224
This commit is contained in:
@@ -95,7 +95,8 @@ import {
|
||||
faArrowDown,
|
||||
faFolderOpen,
|
||||
faIdBadge,
|
||||
faAddressCard
|
||||
faAddressCard,
|
||||
faKeyboard
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/**
|
||||
@@ -201,7 +202,8 @@ export class IconsModule {
|
||||
faArrowDown,
|
||||
faFolderOpen,
|
||||
faIdBadge,
|
||||
faAddressCard
|
||||
faAddressCard,
|
||||
faKeyboard
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<button
|
||||
mat-icon-button class="ml-1" matTooltip="Notifications"
|
||||
[matMenuTriggerFor]="notificationsMenu" (menuClosed)="menuClosed()"
|
||||
[matBadge]="unreadNotifications.length" [matBadgeHidden]="!unreadNotifications.length" matBadgeColor="warn" matBadgeSize="medium"
|
||||
>
|
||||
<fa-icon icon="bell" size="lg"></fa-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu class="mifosx-notifications-menu" #notificationsMenu="matMenu" [overlapTrigger]="false">
|
||||
|
||||
<button mat-menu-item class="unread" *ngFor="let notification of unreadNotifications" [routerLink]="[routeMap[notification.objectType], notification.objectId]">
|
||||
{{ notification.content }}*<br>
|
||||
<span class="time">[{{ notification.createdAt }}]</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item *ngFor="let notification of displayedReadNotifications" [routerLink]="[routeMap[notification.objectType], notification.objectId]">
|
||||
{{ notification.content }}<br>
|
||||
<span class="time">[{{ notification.createdAt }}]</span>
|
||||
</button>
|
||||
|
||||
</mat-menu>
|
||||
@@ -0,0 +1,21 @@
|
||||
.mifosx-notifications-menu {
|
||||
|
||||
max-height: 20rem !important;
|
||||
width: 12.5rem !important;
|
||||
|
||||
.mat-menu-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
[mat-menu-item] {
|
||||
height: 5rem;
|
||||
line-height: 2.2rem;
|
||||
.time {
|
||||
font-size: small;
|
||||
}
|
||||
&.unread {
|
||||
background-color: #E0F7FF !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotificationsTrayComponent } from './notifications-tray.component';
|
||||
|
||||
describe('NotificationsTrayComponent', () => {
|
||||
let component: NotificationsTrayComponent;
|
||||
let fixture: ComponentFixture<NotificationsTrayComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NotificationsTrayComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NotificationsTrayComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
/** RxJS Imports */
|
||||
import { forkJoin } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { NotificationsService } from 'app/notifications/notifications.service';
|
||||
|
||||
/**
|
||||
* Notifications Tray Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-notifications-tray',
|
||||
templateUrl: './notifications-tray.component.html',
|
||||
styleUrls: ['./notifications-tray.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class NotificationsTrayComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Read Notifications */
|
||||
readNotifications: any[] = [];
|
||||
/** Displayed Read Notifications */
|
||||
displayedReadNotifications: any[] = [];
|
||||
/** Unread Notifications */
|
||||
unreadNotifications: any[] = [];
|
||||
/** Timer to refetch notifications every 60 seconds */
|
||||
timer: any;
|
||||
|
||||
/**
|
||||
* Gets router link prefix from notification's objectType attribute
|
||||
* Shares, Savings, Deposits, Loans routes inaccessible because of dependency on entity ID.
|
||||
*/
|
||||
routeMap: any = {
|
||||
'client' : '/clients/',
|
||||
'group' : '/groups/',
|
||||
'loan' : '/loans/',
|
||||
'center' : '/centers/',
|
||||
'shareAccount' : '/shares-accounts/',
|
||||
'fixedDeposit' : '/fixed-deposits-accounts/',
|
||||
'recurringDepositAccount': '/recurringdeposits/',
|
||||
'savingsAccount' : '/savingsaccounts/',
|
||||
'shareProduct': '/products/share-products/',
|
||||
'loanProduct' : '/products/loan-products/'
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {NotificationsService} notificationsService Notifications Service
|
||||
*/
|
||||
constructor(public notificationsService: NotificationsService) {
|
||||
forkJoin([this.notificationsService.getNotifications(true), this.notificationsService.getNotifications(false)])
|
||||
.subscribe((response: any[]) => {
|
||||
this.readNotifications = response[0].pageItems;
|
||||
this.unreadNotifications = response[1].pageItems;
|
||||
this.setNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
setTimeout(() => { this.fetchUnreadNotifications(); }, 60000);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restructures displayed read notifications vis-a-vis unread notifications.
|
||||
*/
|
||||
setNotifications() {
|
||||
const length = this.unreadNotifications.length;
|
||||
this.displayedReadNotifications = length < 9 ? this.readNotifications.slice(0, 9 - length) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively fetch unread notifications.
|
||||
*/
|
||||
fetchUnreadNotifications() {
|
||||
this.notificationsService.getNotifications(false).subscribe((response: any) => {
|
||||
this.unreadNotifications = this.unreadNotifications.concat(response.pageItems);
|
||||
this.setNotifications();
|
||||
});
|
||||
// this.mockNotifications(); // Uncomment for Testing.
|
||||
this.timer = setTimeout(() => { this.fetchUnreadNotifications(); }, 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update read/unread notifications.
|
||||
*/
|
||||
menuClosed() {
|
||||
// Update the server for read notifications.
|
||||
this.notificationsService.updateNotifications().subscribe(() => {});
|
||||
// Update locally for read notifications.
|
||||
this.readNotifications = this.unreadNotifications.concat(this.readNotifications);
|
||||
this.unreadNotifications = [];
|
||||
this.setNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to test notifications in case of faulty backend.
|
||||
*/
|
||||
mockNotifications() {
|
||||
this.notificationsService.getMockUnreadNotification().subscribe((response: any) => {
|
||||
this.unreadNotifications = this.unreadNotifications.concat(response.pageItems);
|
||||
this.setNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" fxLayout="row" fxLayoutGap="2%" [@fadeInOut]>
|
||||
|
||||
<mat-form-field class="search">
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput type="text" [formControl]="query" (keydown.enter)="search()">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="resource">
|
||||
<mat-label>Resource</mat-label>
|
||||
<mat-select [formControl]="resource">
|
||||
<mat-option *ngFor="let option of resourceOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="search-icon">
|
||||
<button mat-icon-button (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
|
||||
<fa-icon icon="search" size="lg"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
.search-bar {
|
||||
.search {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.resource {
|
||||
max-width: 90px;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchToolComponent } from './search-tool.component';
|
||||
|
||||
describe('SearchToolComponent', () => {
|
||||
let component: SearchToolComponent;
|
||||
let fixture: ComponentFixture<SearchToolComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SearchToolComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchToolComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/** Angular Imports */
|
||||
import { Component } from '@angular/core';
|
||||
import { style, animate, transition, trigger } from '@angular/animations';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Search Tool Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-search-tool',
|
||||
templateUrl: './search-tool.component.html',
|
||||
styleUrls: ['./search-tool.component.scss'],
|
||||
animations: [
|
||||
trigger('fadeInOut', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate(500, style({ opacity: 1 }))
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate(500, style({ opacity: 0 }))
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class SearchToolComponent {
|
||||
|
||||
/** Query Form Control */
|
||||
query = new FormControl('');
|
||||
/** Resource Form Control */
|
||||
resource = new FormControl('');
|
||||
|
||||
/** Sets the initial visibility of search input as hidden. Visible if true. */
|
||||
searchVisible = false;
|
||||
/** Resource Options */
|
||||
resourceOptions: any[] = [
|
||||
{
|
||||
name: 'All',
|
||||
value: 'clients,clientIdentifiers,groups,savings,shares,loans'
|
||||
},
|
||||
{
|
||||
name: 'Clients',
|
||||
value: 'clients,clientIdentifiers'
|
||||
},
|
||||
{
|
||||
name: 'Groups',
|
||||
value: 'groups'
|
||||
},
|
||||
{
|
||||
name: 'Savings',
|
||||
value: 'savings'
|
||||
},
|
||||
{
|
||||
name: 'Shares',
|
||||
value: 'shares'
|
||||
},
|
||||
{
|
||||
name: 'Loans',
|
||||
value: 'loans'
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {Router} router Router
|
||||
*/
|
||||
constructor(private router: Router) {
|
||||
this.resource.patchValue('clients,clientIdentifiers,groups,savings,shares,loans');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of search input with fadeInOut animation.
|
||||
*/
|
||||
toggleSearchVisibility() {
|
||||
this.searchVisible = !this.searchVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches server for query and resource.
|
||||
*/
|
||||
search() {
|
||||
const queryParams: any = {
|
||||
query: this.query.value,
|
||||
resource: this.resource.value
|
||||
};
|
||||
this.router.navigate(['/search'], { queryParams: queryParams });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,12 +16,14 @@ import { ChangePasswordDialogComponent } from './change-password-dialog/change-p
|
||||
import { EnableDialogComponent } from './enable-dialog/enable-dialog.component';
|
||||
import { DisableDialogComponent } from './disable-dialog/disable-dialog.component';
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
|
||||
import { ErrorDialogComponent } from './error-dialog/error-dialog.component';
|
||||
import { NotificationsTrayComponent } from './notifications-tray/notifications-tray.component';
|
||||
import { SearchToolComponent } from './search-tool/search-tool.component';
|
||||
import { KeyboardShortcutsDialogComponent } from './keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component';
|
||||
|
||||
/** Custom Modules */
|
||||
import { IconsModule } from './icons.module';
|
||||
import { MaterialModule } from './material.module';
|
||||
import { ErrorDialogComponent } from './error-dialog/error-dialog.component';
|
||||
|
||||
/**
|
||||
* Shared Module
|
||||
@@ -49,19 +51,23 @@ import { ErrorDialogComponent } from './error-dialog/error-dialog.component';
|
||||
DisableDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
KeyboardShortcutsDialogComponent,
|
||||
ErrorDialogComponent
|
||||
ErrorDialogComponent,
|
||||
NotificationsTrayComponent,
|
||||
SearchToolComponent
|
||||
],
|
||||
exports: [
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent,
|
||||
ThemePickerComponent,
|
||||
NotificationsTrayComponent,
|
||||
SearchToolComponent,
|
||||
ErrorDialogComponent,
|
||||
CommonModule,
|
||||
IconsModule,
|
||||
MaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ErrorDialogComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
||||
Reference in New Issue
Block a user