@@ -28,6 +28,7 @@ import { RouteReusableStrategy } from './route/route-reusable-strategy';
|
|||||||
|
|
||||||
/** Custom Modules */
|
/** Custom Modules */
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
|
||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { ShellComponent } from './shell/shell.component';
|
import { ShellComponent } from './shell/shell.component';
|
||||||
@@ -44,6 +45,7 @@ import { ContentComponent } from './shell/content/content.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
NotificationsModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
|||||||
@@ -61,14 +61,7 @@
|
|||||||
|
|
||||||
<mifosx-theme-picker fxHide.lt-md></mifosx-theme-picker>
|
<mifosx-theme-picker fxHide.lt-md></mifosx-theme-picker>
|
||||||
|
|
||||||
<!--
|
<mifosx-notifications-tray fxHide.lt-md></mifosx-notifications-tray>
|
||||||
TODO: Notifications
|
|
||||||
Separate into its own component in shared module for reuse.
|
|
||||||
Implement functionality.
|
|
||||||
-->
|
|
||||||
<button mat-icon-button class="ml-1" matTooltip="Notifications">
|
|
||||||
<fa-icon icon="bell" size="lg"></fa-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button mat-icon-button class="ml-1 img-button" [matMenuTriggerFor]="applicationMenu">
|
<button mat-icon-button class="ml-1 img-button" [matMenuTriggerFor]="applicationMenu">
|
||||||
<img src="assets/images/user_placeholder.png">
|
<img src="assets/images/user_placeholder.png">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="select-row" [routerLink]="[routeMap[row.objectType], row.objectId]"></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.select-row:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -20,6 +20,23 @@ export class NotificationsPageComponent implements OnInit {
|
|||||||
/** Data source for notifications table. */
|
/** Data source for notifications table. */
|
||||||
dataSource: MatTableDataSource<any>;
|
dataSource: MatTableDataSource<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' : '/sharesaccounts/',
|
||||||
|
'fixedDeposit' : '/fixed-deposits-accounts/',
|
||||||
|
'recurringDepositAccount': '/recurringdeposits/',
|
||||||
|
'savingsAccount' : '/savingsaccounts/',
|
||||||
|
'shareProduct': '/products/share-products/',
|
||||||
|
'loanProduct' : '/products/loan-products/'
|
||||||
|
};
|
||||||
|
|
||||||
/** Paginator for notifications table. */
|
/** Paginator for notifications table. */
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
/** Sorter for notifications table. */
|
/** Sorter for notifications table. */
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<button mat-menu-item class="view-all" [routerLink]="['/notifications']">View All Notifications</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, &.view-all {
|
||||||
|
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 '../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' : '/sharesaccounts/',
|
||||||
|
'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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,18 +8,23 @@ import { NotificationsRoutingModule } from './notifications-routing.module';
|
|||||||
|
|
||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { NotificationsPageComponent } from './notifications-page/notifications-page.component';
|
import { NotificationsPageComponent } from './notifications-page/notifications-page.component';
|
||||||
|
import { NotificationsTrayComponent } from './notifications-tray/notifications-tray.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Module
|
* Notifications Module
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
NotificationsPageComponent
|
NotificationsPageComponent,
|
||||||
|
NotificationsTrayComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
PipesModule,
|
PipesModule,
|
||||||
NotificationsRoutingModule
|
NotificationsRoutingModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
NotificationsTrayComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class NotificationsModule { }
|
export class NotificationsModule { }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification service.
|
* Notification service.
|
||||||
@@ -26,4 +26,33 @@ export class NotificationsService {
|
|||||||
return this.http.get('/notifications', { params: httpParams });
|
return this.http.get('/notifications', { params: httpParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} Notifications.
|
||||||
|
*/
|
||||||
|
updateNotifications(): Observable<any> {
|
||||||
|
return this.http.put('/notifications', { });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} Mock Unread Notifications for Testing.
|
||||||
|
*/
|
||||||
|
getMockUnreadNotification(): Observable<any> {
|
||||||
|
const date = new Date();
|
||||||
|
return of({
|
||||||
|
'totalFilteredRecords': 1,
|
||||||
|
'pageItems': [
|
||||||
|
{ 'id': Math.floor(Math.random() * 100),
|
||||||
|
'objectType': 'client',
|
||||||
|
'objectId': Math.floor(Math.random() * 10),
|
||||||
|
'action': 'clientCreated',
|
||||||
|
'actorId': 2,
|
||||||
|
'content': 'Client Created',
|
||||||
|
'isRead': false,
|
||||||
|
'isSystemGenerated': false,
|
||||||
|
'createdAt': `${date.toLocaleDateString()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ import {
|
|||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatTreeModule
|
MatTreeModule,
|
||||||
|
MatBadgeModule
|
||||||
} from '@angular/material';
|
} from '@angular/material';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +82,8 @@ import {
|
|||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatTreeModule
|
MatTreeModule,
|
||||||
|
MatBadgeModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user