@@ -29,6 +29,7 @@ import { RouteReusableStrategy } from './route/route-reusable-strategy';
|
||||
/** Custom Modules */
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { SearchModule } from 'app/search/search.module';
|
||||
|
||||
/** Custom Components */
|
||||
import { ShellComponent } from './shell/shell.component';
|
||||
@@ -45,6 +46,7 @@ import { ContentComponent } from './shell/content/content.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
SearchModule,
|
||||
NotificationsModule,
|
||||
HttpClientModule,
|
||||
TranslateModule,
|
||||
|
||||
@@ -41,21 +41,7 @@
|
||||
|
||||
<span fxFlex="auto"></span>
|
||||
|
||||
<!--
|
||||
TODO: Search
|
||||
Separate into its own component in shared module for reuse.
|
||||
Implement functionality.
|
||||
-->
|
||||
<div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" [@fadeInOut]>
|
||||
<mat-form-field class="search">
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput type="text" (focusout)="toggleSearchVisibility()">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<button mat-icon-button class="ml-1" (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
|
||||
<fa-icon icon="search" size="lg"></fa-icon>
|
||||
</button>
|
||||
<mifosx-search-tool></mifosx-search-tool>
|
||||
|
||||
<mifosx-language-selector class="ml-1 language" fxHide.lt-md></mifosx-language-selector>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { style, animate, transition, trigger } from '@angular/animations';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/** rxjs Imports */
|
||||
@@ -18,18 +17,7 @@ import { AuthenticationService } from '../../authentication/authentication.servi
|
||||
@Component({
|
||||
selector: 'mifosx-toolbar',
|
||||
templateUrl: './toolbar.component.html',
|
||||
styleUrls: ['./toolbar.component.scss'],
|
||||
animations: [
|
||||
trigger('fadeInOut', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate(500, style({ opacity: 1 }))
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate(500, style({ opacity: 0 }))
|
||||
])
|
||||
])
|
||||
]
|
||||
styleUrls: ['./toolbar.component.scss']
|
||||
})
|
||||
export class ToolbarComponent implements OnInit {
|
||||
|
||||
@@ -39,8 +27,6 @@ export class ToolbarComponent implements OnInit {
|
||||
map(result => result.matches)
|
||||
);
|
||||
|
||||
/** Sets the initial visibility of search input as hidden. Visible if true. */
|
||||
searchVisible = false;
|
||||
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
|
||||
sidenavCollapsed = true;
|
||||
|
||||
@@ -84,13 +70,6 @@ export class ToolbarComponent implements OnInit {
|
||||
this.collapse.emit(this.sidenavCollapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of search input with fadeInOut animation.
|
||||
*/
|
||||
toggleSearchVisibility() {
|
||||
this.searchVisible = !this.searchVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the authenticated user and redirects to login page.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
|
||||
/** Routing Imports */
|
||||
import { Route } from '../core/route/route.service';
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="container">
|
||||
|
||||
<mat-card fxLayout="column" fxLayoutGap="2%">
|
||||
|
||||
<mat-error *ngIf="overload">Searched query resulted more than 200 records, showing first 200 records.</mat-error>
|
||||
|
||||
<mat-list>
|
||||
|
||||
<mat-list-item (click)="navigate(entity)" *ngFor="let entity of searchResults">
|
||||
<h3 matLine><span class="link">{{ entity.entityName }}</span> | #{{ entity.entityAccountNo }}</h3>
|
||||
<p matLine>
|
||||
<span>Entity: {{ entity.entityType }}
|
||||
| {{ ['CLIENT','GROUP', 'CENTER'].includes(entity.entityType) ? 'Office' : 'Client' }}: {{ entity.parentName }}
|
||||
</span>
|
||||
</p>
|
||||
</mat-list-item>
|
||||
|
||||
</mat-list>
|
||||
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
mat-list {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
.link {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
width: fit-content;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
margin-block: 20px;
|
||||
&:hover{
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
let component: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SearchPageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/** Angular Imports */
|
||||
import { Component } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Search Page Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-search-page',
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search-page.component.scss']
|
||||
})
|
||||
export class SearchPageComponent {
|
||||
|
||||
/** Search Results */
|
||||
searchResults: any;
|
||||
/** Flags if number of search results exceed 200 */
|
||||
overload: boolean;
|
||||
|
||||
/**
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {Router} router Router
|
||||
*/
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router) {
|
||||
this.route.data.subscribe(( data: { searchResults: any }) => {
|
||||
this.searchResults = data.searchResults;
|
||||
this.overload = this.searchResults.length > 200 ? true : false;
|
||||
if (this.overload) {
|
||||
this.searchResults = this.searchResults.slice(0, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns link to entity view page.
|
||||
* @param {any} entity Entity
|
||||
*/
|
||||
navigate(entity: any) {
|
||||
switch (entity.entityType) {
|
||||
case 'CLIENT':
|
||||
this.router.navigate(['clients', entity.entityId]);
|
||||
break;
|
||||
case 'CENTER':
|
||||
this.router.navigate(['centers', entity.entityId]);
|
||||
break;
|
||||
case 'GROUP':
|
||||
this.router.navigate(['groups', entity.entityId]);
|
||||
break;
|
||||
case 'SHARE':
|
||||
this.router.navigate(['clients', entity.parentId, 'sharesaccounts', entity.entityId]);
|
||||
break;
|
||||
case 'SAVING':
|
||||
this.router.navigate(['clients', entity.parentId, 'savingsaccounts', entity.entityId]);
|
||||
break;
|
||||
case 'LOAN':
|
||||
this.router.navigate(['clients', entity.parentId, 'loans', entity.entityId]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
/** Routing Imports */
|
||||
import { Route } from '../core/route/route.service';
|
||||
|
||||
/** Translation Imports */
|
||||
import { extract } from '../core/i18n/i18n.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { SearchPageComponent } from './search-page/search-page.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
import { SearchResolver } from './search.resolver';
|
||||
|
||||
/** Search Routes */
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'search',
|
||||
component: SearchPageComponent,
|
||||
data: { title: extract('Search'), breadcrumb: 'Search' },
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
|
||||
resolve: {
|
||||
searchResults: SearchResolver
|
||||
}
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
providers: [SearchResolver],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class SearchRoutingModule { }
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
/** Custom Modules */
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SearchRoutingModule } from './search-routing.module';
|
||||
|
||||
/** Custom Components */
|
||||
import { SearchToolComponent } from './search-tool/search-tool.component';
|
||||
import { SearchPageComponent } from './search-page/search-page.component';
|
||||
|
||||
/**
|
||||
* Search Module
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [SearchToolComponent, SearchPageComponent],
|
||||
imports: [
|
||||
SharedModule,
|
||||
SearchRoutingModule
|
||||
],
|
||||
exports: [
|
||||
SearchToolComponent
|
||||
]
|
||||
})
|
||||
export class SearchModule { }
|
||||
@@ -0,0 +1,33 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { SearchService } from './search.service';
|
||||
|
||||
/**
|
||||
* Search Results data resolver.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchResolver implements Resolve<Object> {
|
||||
|
||||
/**
|
||||
* @param {SearchService} searchService Notifications service.
|
||||
*/
|
||||
constructor(private searchService: SearchService) {}
|
||||
|
||||
/**
|
||||
* Returns the Search Resultsdata.
|
||||
* @param {ActivatedRouteSnapshot} route Route Snapshot
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot): Observable<any> {
|
||||
const query = route.queryParams['query'];
|
||||
const resource = route.queryParams['resource'];
|
||||
return this.searchService.getSearchResults(query, resource);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchService } from './search.service';
|
||||
|
||||
describe('SearchService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: SearchService = TestBed.get(SearchService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Search service.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SearchService {
|
||||
|
||||
/**
|
||||
* @param {HttpClient} http Http Client to send requests.
|
||||
*/
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
/**
|
||||
* @param {string} query Query String
|
||||
* @param {string} resource Entity resource
|
||||
* @returns {Observable<any>} Search Results.
|
||||
*/
|
||||
getSearchResults(query: string, resource: string): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('exactMatch', 'false')
|
||||
.set('query', query)
|
||||
.set('resource', resource);
|
||||
return this.http.get('/search', { params: httpParams });
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user