Merge pull request #1583 from josehernandezfintecheandomx/enhancement/office_tree_view

Office hierarchy view as tree
This commit is contained in:
Adam Saghy
2022-11-25 21:46:53 +01:00
committed by GitHub
6 changed files with 360 additions and 21 deletions
@@ -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>&nbsp;&nbsp;
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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>.