From 7d8c94698a893a1f25940af3a5b7f770e3d36cb7 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Fri, 25 Nov 2022 14:03:59 -0600 Subject: [PATCH] Office hierarchy view as tree --- .../organization/offices/office-node.model.ts | 34 ++++ .../office-tree-service.service.spec.ts | 16 ++ .../offices/office-tree-service.service.ts | 81 +++++++++ .../offices/offices.component.html | 158 ++++++++++++++++-- .../offices/offices.component.scss | 26 +++ .../organization/offices/offices.component.ts | 66 +++++++- 6 files changed, 360 insertions(+), 21 deletions(-) create mode 100644 src/app/organization/offices/office-node.model.ts create mode 100644 src/app/organization/offices/office-tree-service.service.spec.ts create mode 100644 src/app/organization/offices/office-tree-service.service.ts diff --git a/src/app/organization/offices/office-node.model.ts b/src/app/organization/offices/office-node.model.ts new file mode 100644 index 000000000..8a0669eed --- /dev/null +++ b/src/app/organization/offices/office-node.model.ts @@ -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 = []; + } + +} diff --git a/src/app/organization/offices/office-tree-service.service.spec.ts b/src/app/organization/offices/office-tree-service.service.spec.ts new file mode 100644 index 000000000..557e75523 --- /dev/null +++ b/src/app/organization/offices/office-tree-service.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/organization/offices/office-tree-service.service.ts b/src/app/organization/offices/office-tree-service.service.ts new file mode 100644 index 000000000..10a5fa04a --- /dev/null +++ b/src/app/organization/offices/office-tree-service.service.ts @@ -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([]); + + /** + * 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; + } + +} diff --git a/src/app/organization/offices/offices.component.html b/src/app/organization/offices/offices.component.html index ccc568feb..85e2f40c8 100644 --- a/src/app/organization/offices/offices.component.html +++ b/src/app/organization/offices/offices.component.html @@ -1,15 +1,17 @@
-
- -
+ + + + + + + +
@@ -17,43 +19,44 @@
-
+
-
+
- Filter + {{ 'Filter'| translate}}
-
+ +
- + - + - + - + @@ -66,6 +69,129 @@ + + + +
+ +
+ + +
+ +
+ +
+ + + + + + + + +
  • + + + {{ node.name }} + +
  • +
    + + +
  • +
    + + + {{ node.name }} + +
    +
      + +
    +
  • +
    + +
    + +
    + +
    + +
    + +
    + + + + + +
    + + + {{ office.name }} + + +
    + +
    + +
    + + + + + +
    + +
    + {{'Parent Office'|translate}} +
    + +
    + {{ office.parentName }} +
    + +
    + {{' Opened On'|translate}} +
    + +
    + {{ office.openingDate | date }} +
    + +
    + {{'External ID'|translate}} +
    + +
    + {{ office.externalId }} +
    + +
    + +
    + + + +
    + +
    + +
    + +
    + +
    diff --git a/src/app/organization/offices/offices.component.scss b/src/app/organization/offices/offices.component.scss index 24edb63bb..9b181acc8 100644 --- a/src/app/organization/offices/offices.component.scss +++ b/src/app/organization/offices/offices.component.scss @@ -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; + } +} diff --git a/src/app/organization/offices/offices.component.ts b/src/app/organization/offices/offices.component.ts index 12e232578..7424c12ec 100644 --- a/src/app/organization/offices/offices.component.ts +++ b/src/app/organization/offices/offices.component.ts @@ -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; + /** Nested tree control for offices tree. */ + nestedTreeControl: NestedTreeControl; + /** Nested tree data source for offices tree. */ + nestedTreeDataSource: MatTreeNestedDataSource; + /** 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; /* 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(this.getChildren); + this.nestedTreeDataSource = new MatTreeNestedDataSource(); } /** @@ -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.
    Office Name {{' Office Name '|translate}} {{ offices.name }} External ID {{' External ID '|translate}} {{ offices.externalId }} Parent Office {{' Parent Office '|translate}} {{ offices.parentName }} Opened On {{' Opened On '|translate}} {{ offices.openingDate | dateFormat }}