Merge pull request #1583 from josehernandezfintecheandomx/enhancement/office_tree_view
Office hierarchy view as tree
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Office Node model.
|
||||
*/
|
||||
export class OfficeNode {
|
||||
|
||||
/** Office Node children. */
|
||||
children: OfficeNode[];
|
||||
|
||||
name: string;
|
||||
id: string;
|
||||
parentId: string;
|
||||
hierarchy: string;
|
||||
externalId: string;
|
||||
parentName: string;
|
||||
openingDate: string;
|
||||
|
||||
constructor(name: string,
|
||||
id: string = '',
|
||||
parentId: string = '',
|
||||
hierarchy: string = '',
|
||||
externalId: string = '',
|
||||
parentName: string = '',
|
||||
openingDate: string = '') {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.parentId = parentId;
|
||||
this.hierarchy = hierarchy;
|
||||
this.externalId = externalId;
|
||||
this.parentName = parentName;
|
||||
this.openingDate = openingDate;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OfficeTreeServiceService } from './office-tree-service.service';
|
||||
|
||||
describe('OfficeTreeServiceService', () => {
|
||||
let service: OfficeTreeServiceService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(OfficeTreeServiceService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
/** Custom Components */
|
||||
import { OfficeNode } from './office-node.model';
|
||||
|
||||
/**
|
||||
* Office tree service.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfficeTreeService {
|
||||
|
||||
/** Office data. */
|
||||
officesData: any;
|
||||
/** Offices tree data behavior subject to represent offices tree nodes. */
|
||||
treeDataChange = new BehaviorSubject<OfficeNode[]>([]);
|
||||
|
||||
/**
|
||||
* Gets the offices tree nodes.
|
||||
*/
|
||||
get treeData(): OfficeNode[] { return this.treeDataChange.value; }
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Builds the offices tree and emits the value.
|
||||
* @param {any} officesData Offices data.
|
||||
*/
|
||||
initialize(officesData: any) {
|
||||
const treeData = this.buildOfficeTree(officesData);
|
||||
this.treeDataChange.next(treeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the offices tree.
|
||||
* @param {any} officeData Offices data.
|
||||
* @returns {OfficeNode} Offices tree nodes.
|
||||
*/
|
||||
buildOfficeTree(officesData: any): OfficeNode[] {
|
||||
const officeTree: OfficeNode[] = [];
|
||||
|
||||
// Header nodes
|
||||
const mainOffice = officesData.find((office: any) => office.hierarchy === '.');
|
||||
officeTree.push(new OfficeNode(mainOffice.name, mainOffice.id));
|
||||
|
||||
// Sort by parent id (so that child nodes can be added properly)
|
||||
officesData.sort((officeOne: any, officeTwo: any) => {
|
||||
if (!officeOne.parentId) {
|
||||
officeOne.parentId = 0;
|
||||
}
|
||||
return officeOne.parentId - officeTwo.parentId;
|
||||
});
|
||||
|
||||
const offices: OfficeNode[] = [];
|
||||
|
||||
// Add offices to any array where index for each is denoted by its id
|
||||
for (const office of officesData) {
|
||||
offices[office.id] =
|
||||
new OfficeNode(office.name, office.id, office.parentId, office.hierarchy, office.externalId, office.parentName, office.openingDate);
|
||||
}
|
||||
|
||||
// Construct offices tree by adding all nodes belonging to headers (with parent id = 0),
|
||||
// and rest as children to respective parent nodes.
|
||||
for (const office of officesData) {
|
||||
if (office.hierarchy !== '.') {
|
||||
if (office.parentId === 1) {
|
||||
officeTree[0].children.push(offices[office.id]);
|
||||
} else {
|
||||
offices[office.parentId].children.push(offices[office.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return officeTree;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
<div class="container m-b-20" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="20px">
|
||||
|
||||
<div #buttonTreeView class="in-block">
|
||||
<button mat-raised-button color="primary">
|
||||
<fa-icon icon="sitemap"></fa-icon>
|
||||
Tree View
|
||||
</button>
|
||||
</div>
|
||||
<mat-button-toggle-group [formControl]="viewGroup">
|
||||
<mat-button-toggle value="listView">
|
||||
<fa-icon icon="list"></fa-icon>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="treeView">
|
||||
<fa-icon icon="sitemap"></fa-icon>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<div #buttonCreateOffice class="in-block">
|
||||
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_OFFICE'">
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
Create Office
|
||||
</button>
|
||||
</div>
|
||||
@@ -17,43 +19,44 @@
|
||||
<div #buttonImportOffices class="in-block">
|
||||
<button mat-raised-button color="primary" *mifosxHasPermission="'CREATE_OFFICE'"
|
||||
[routerLink]="['/organization', 'bulk-import', 'Offices']">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
<fa-icon icon="upload" class="m-r-10"></fa-icon>
|
||||
Import Offices
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="container" [hidden]="viewGroup.value !== 'listView'">
|
||||
|
||||
<div #filter fxLayout="row" fxLayoutGap="20px">
|
||||
<div fxLayout="row" fxLayoutGap="20px">
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label>Filter</mat-label>
|
||||
<mat-label>{{ 'Filter'| translate}}</mat-label>
|
||||
<input matInput (keyup)="applyFilter($event.target.value)">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div #tableOffices class="mat-elevation-z8">
|
||||
<!-- List View -->
|
||||
<div class="mat-elevation-z8">
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Office Name </th>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{' Office Name '|translate}}</th>
|
||||
<td mat-cell *matCellDef="let offices"> {{ offices.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="externalId">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> External ID </th>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{' External ID '|translate}}</th>
|
||||
<td mat-cell *matCellDef="let offices"> {{ offices.externalId }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="parentName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Parent Office </th>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{' Parent Office '|translate}}</th>
|
||||
<td mat-cell *matCellDef="let offices"> {{ offices.parentName }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="openingDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Opened On </th>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{' Opened On '|translate}}</th>
|
||||
<td mat-cell *matCellDef="let offices"> {{ offices.openingDate | dateFormat }} </td>
|
||||
</ng-container>
|
||||
|
||||
@@ -66,6 +69,129 @@
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tree View -->
|
||||
<div class="container" [hidden]="viewGroup.value !== 'treeView'">
|
||||
|
||||
<div class="m-b-20" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="20px">
|
||||
<button mat-raised-button (click)="nestedTreeControl.expandAll()">
|
||||
{{'Expand All'|translate}}
|
||||
</button>
|
||||
<button mat-raised-button (click)="nestedTreeControl.collapseAll()">
|
||||
{{'Collapse All'|translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div fxLayout="row" fxLayoutGap="4%" fxLayout.lt-md="column">
|
||||
|
||||
<div fxFlex>
|
||||
|
||||
<mat-card>
|
||||
|
||||
<mat-card-content>
|
||||
|
||||
<mat-tree [dataSource]="nestedTreeDataSource" [treeControl]="nestedTreeControl" class="office-tree">
|
||||
|
||||
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
|
||||
<li class="mat-tree-node">
|
||||
<button mat-icon-button disabled></button>
|
||||
<span (click)="viewOfficeNode(node)" class="m-r-10">
|
||||
{{ node.name }}
|
||||
</span>
|
||||
</li>
|
||||
</mat-tree-node>
|
||||
|
||||
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasNestedChild">
|
||||
<li>
|
||||
<div class="mat-tree-node">
|
||||
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name">
|
||||
<fa-icon class="mat-icon-rtl-mirror"
|
||||
icon="{{ nestedTreeControl.isExpanded(node) ? 'chevron-down' : 'chevron-right' }}"></fa-icon>
|
||||
</button>
|
||||
<span (click)="viewOfficeNode(node)" class="m-r-10">
|
||||
{{ node.name }}
|
||||
</span>
|
||||
</div>
|
||||
<ul [class.office-tree-invisible]="!nestedTreeControl.isExpanded(node)">
|
||||
<ng-container matTreeNodeOutlet></ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</mat-nested-tree-node>
|
||||
|
||||
</mat-tree>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
|
||||
<div fxFlex="48%" *ngIf="office">
|
||||
|
||||
<mat-card>
|
||||
|
||||
<mat-card-content>
|
||||
|
||||
<div fxLayout="row">
|
||||
|
||||
<mat-card-title>
|
||||
{{ office.name }}
|
||||
</mat-card-title>
|
||||
|
||||
<div fxFlex fxLayoutAlign="end">
|
||||
<button mat-icon-button (click)="closeOffice()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<mat-tab-group>
|
||||
|
||||
<mat-tab label="General">
|
||||
|
||||
<div fxLayout="row wrap" class="content">
|
||||
|
||||
<div fxFlex="50%" class="mat-body-strong" *ngIf="office.parentId">
|
||||
{{'Parent Office'|translate}}
|
||||
</div>
|
||||
|
||||
<div fxFlex="50%" *ngIf="office.parentId">
|
||||
{{ office.parentName }}
|
||||
</div>
|
||||
|
||||
<div fxFlex="50%" class="mat-body-strong">
|
||||
{{' Opened On'|translate}}
|
||||
</div>
|
||||
|
||||
<div fxFlex="50%">
|
||||
{{ office.openingDate | date }}
|
||||
</div>
|
||||
|
||||
<div fxFlex="50%" class="mat-body-strong" *ngIf="office.externalId">
|
||||
{{'External ID'|translate}}
|
||||
</div>
|
||||
|
||||
<div fxFlex="50%" *ngIf="office.externalId">
|
||||
{{ office.externalId }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab *ngFor="let dataTable of dataTablesData" label="{{ dataTable.registeredTableName }}"></mat-tab>
|
||||
|
||||
</mat-tab-group>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,3 +5,29 @@ table {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
div {
|
||||
margin: 1rem 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.office-tree {
|
||||
ul,
|
||||
li {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.office-tree-invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import { of } from 'rxjs';
|
||||
/** Custom Services */
|
||||
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
|
||||
import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { OfficeNode } from './office-node.model';
|
||||
import { NestedTreeControl } from '@angular/cdk/tree';
|
||||
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||
import { OfficeTreeService } from './office-tree-service.service';
|
||||
|
||||
/**
|
||||
* Offices component.
|
||||
@@ -21,6 +26,8 @@ import { ConfigurationWizardService } from '../../configuration-wizard/configura
|
||||
styleUrls: ['./offices.component.scss']
|
||||
})
|
||||
export class OfficesComponent implements OnInit, AfterViewInit {
|
||||
/** Button toggle group form control for type of view. (list/tree) */
|
||||
viewGroup = new FormControl('listView');
|
||||
|
||||
/** Offices data. */
|
||||
officesData: any;
|
||||
@@ -28,12 +35,21 @@ export class OfficesComponent implements OnInit, AfterViewInit {
|
||||
displayedColumns: string[] = ['name', 'externalId', 'parentName', 'openingDate'];
|
||||
/** Data source for offices table. */
|
||||
dataSource: MatTableDataSource<any>;
|
||||
/** Nested tree control for offices tree. */
|
||||
nestedTreeControl: NestedTreeControl<OfficeNode>;
|
||||
/** Nested tree data source for offices tree. */
|
||||
nestedTreeDataSource: MatTreeNestedDataSource<OfficeNode>;
|
||||
/** Selected Office. */
|
||||
office: OfficeNode;
|
||||
/** Office data tables. */
|
||||
dataTablesData: any;
|
||||
|
||||
/** Paginator for offices table. */
|
||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||
/** Sorter for offices table. */
|
||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||
|
||||
|
||||
/* Reference of tree view button */
|
||||
@ViewChild('buttonTreeView') buttonTreeView: ElementRef<any>;
|
||||
/* Template for popover on tree view button */
|
||||
@@ -47,16 +63,22 @@ export class OfficesComponent implements OnInit, AfterViewInit {
|
||||
* Retrieves the offices data from `resolve`.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
* @param {Router} router Router.
|
||||
* @param {OfficeTreeService} officeTreeService office tree service.
|
||||
* @param {ConfigurationWizardService} configurationWizardService ConfigurationWizard Service.
|
||||
* @param {PopoverService} popoverService PopoverService.
|
||||
*/
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private configurationWizardService: ConfigurationWizardService,
|
||||
private popoverService: PopoverService) {
|
||||
this.route.data.subscribe(( data: { offices: any }) => {
|
||||
private router: Router,
|
||||
private officeTreeService: OfficeTreeService,
|
||||
private configurationWizardService: ConfigurationWizardService,
|
||||
private popoverService: PopoverService) {
|
||||
this.route.data.subscribe(( data: { offices: any, officeDataTables: any }) => {
|
||||
this.officesData = data.offices;
|
||||
officeTreeService.initialize(this.officesData);
|
||||
this.dataTablesData = data.officeDataTables;
|
||||
});
|
||||
this.nestedTreeControl = new NestedTreeControl<OfficeNode>(this.getChildren);
|
||||
this.nestedTreeDataSource = new MatTreeNestedDataSource<OfficeNode>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,8 +92,13 @@ export class OfficesComponent implements OnInit, AfterViewInit {
|
||||
/**
|
||||
* Sets the offices table.
|
||||
*/
|
||||
ngOnInit() {
|
||||
ngOnInit() {
|
||||
this.setOffices();
|
||||
this.officeTreeService.treeDataChange.subscribe((officeTreeData: OfficeNode[]) => {
|
||||
this.nestedTreeDataSource.data = officeTreeData;
|
||||
this.nestedTreeControl.expand(this.nestedTreeDataSource.data[0]);
|
||||
this.nestedTreeControl.dataNodes = officeTreeData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +110,35 @@ export class OfficesComponent implements OnInit, AfterViewInit {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* View selected office.
|
||||
* @param {OfficeNode} office Office to be viewed.
|
||||
*/
|
||||
viewOfficeNode(office: OfficeNode) {
|
||||
if (office.id) {
|
||||
this.office = office;
|
||||
} else {
|
||||
delete this.office;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current view for office
|
||||
*/
|
||||
closeOffice() {
|
||||
delete this.office;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if selected node in tree has children.
|
||||
*/
|
||||
hasNestedChild = (_: number, node: OfficeNode) => node.children.length;
|
||||
|
||||
/**
|
||||
* Gets the children of selected node in tree.
|
||||
*/
|
||||
private getChildren = (node: OfficeNode) => of(node.children);
|
||||
|
||||
/**
|
||||
* Popover function
|
||||
* @param template TemplateRef<any>.
|
||||
|
||||
Reference in New Issue
Block a user