From 039a97d2d9edbcc0dfb919801f347a8b648ee30d Mon Sep 17 00:00:00 2001 From: Ramveer Date: Wed, 31 Aug 2022 16:40:27 +0530 Subject: [PATCH] feat: Holiday office multiselect replaced with multiselect --- .../create-holiday/checklist-db.class.ts | 46 ++++ .../create-holiday.component.html | 26 ++- .../create-holiday.component.ts | 215 +++++++++++++++++- .../create-holiday/create-holiday.service.ts | 17 ++ .../create-holiday/todo-flat-item.class.ts | 10 + .../create-holiday/todo-item.class.ts | 9 + src/app/organization/organization.module.ts | 14 ++ 7 files changed, 326 insertions(+), 11 deletions(-) create mode 100644 src/app/organization/holidays/create-holiday/checklist-db.class.ts create mode 100644 src/app/organization/holidays/create-holiday/create-holiday.service.ts create mode 100644 src/app/organization/holidays/create-holiday/todo-flat-item.class.ts create mode 100644 src/app/organization/holidays/create-holiday/todo-item.class.ts diff --git a/src/app/organization/holidays/create-holiday/checklist-db.class.ts b/src/app/organization/holidays/create-holiday/checklist-db.class.ts new file mode 100644 index 000000000..fe641ac40 --- /dev/null +++ b/src/app/organization/holidays/create-holiday/checklist-db.class.ts @@ -0,0 +1,46 @@ +import { BehaviorSubject } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { TodoItemNode } from './todo-item.class'; + +@Injectable({ + providedIn: 'root' +}) +export class ChecklistDatabase { + + + // rxjs functionality to update DOM via subscribe + dataChange = new BehaviorSubject([]); + get data(): TodoItemNode[] { + return this.dataChange.value; + } + TREE_DATA = {}; + + constructor() { + } + + // called every time the data in route changes + initialize(trie: any) { + this.TREE_DATA = trie; + const data = this.buildFileTree(this.TREE_DATA, 0); + this.dataChange.next(data); + } + + // builds hierarchical tree of TodoItemNodes + buildFileTree(obj: { [key: string]: any }, level: number): TodoItemNode[] { + return Object.keys(obj).reduce((accumulator, key) => { + const value = obj[key]; + const node = new TodoItemNode(); + node.item = key; + + if (value != null) { + if (typeof value === 'object') { + node.children = this.buildFileTree(value, level + 1); + } else { + node.item = value; + } + } + + return accumulator.concat(node); + }, []); + } +} diff --git a/src/app/organization/holidays/create-holiday/create-holiday.component.html b/src/app/organization/holidays/create-holiday/create-holiday.component.html index bbba20675..ecc07418a 100644 --- a/src/app/organization/holidays/create-holiday/create-holiday.component.html +++ b/src/app/organization/holidays/create-holiday/create-holiday.component.html @@ -64,7 +64,7 @@ - + + + + + + + + {{officesDict[node.item].name}} + + + + + {{ officesDict[node.item].name }} + + diff --git a/src/app/organization/holidays/create-holiday/create-holiday.component.ts b/src/app/organization/holidays/create-holiday/create-holiday.component.ts index c72250f39..4a1152356 100644 --- a/src/app/organization/holidays/create-holiday/create-holiday.component.ts +++ b/src/app/organization/holidays/create-holiday/create-holiday.component.ts @@ -1,12 +1,19 @@ /** Angular Imports. */ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { SelectionModel } from '@angular/cdk/collections'; +import { Component, OnInit, ViewChild, Injectable } from '@angular/core'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Dates } from 'app/core/utils/dates'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; +import { FlatTreeControl } from '@angular/cdk/tree'; /** Custom Services. */ import { OrganizationService } from 'app/organization/organization.service'; import { SettingsService } from 'app/settings/settings.service'; +import { TodoItemNode } from './todo-item.class'; +import { TodoItemFlatNode } from './todo-flat-item.class'; +import { ChecklistDatabase } from './checklist-db.class'; +import { CreateHoliday } from './create-holiday.service'; /** * Create Holiday component. @@ -29,6 +36,35 @@ export class CreateHolidayComponent implements OnInit { minDate = new Date(2000, 0, 1); /** Maximum Date allowed. */ maxDate = new Date(2100, 0, 1); + // Stores office data in a trie-like structure + officesTrie: any; + // Stores office data for access from DOM via Office ID + officesDict = {}; + + // Angular Material Tree Configuration start ------------ + + /** Map from flat node to nested node. This helps us finding the nested node to be modified */ + flatNodeMap = new Map(); + + /** Map from nested node to flattened node. This helps us to keep the same object for selection */ + nestedNodeMap = new Map(); + + /** A selected parent node to be inserted */ + selectedParent: TodoItemFlatNode | null = null; + + /** The new item's name */ + newItemName = ''; + + treeControl: FlatTreeControl; + + treeFlattener: MatTreeFlattener; + + dataSource: MatTreeFlatDataSource; + + /** The selection for checklist */ + checklistSelection = new SelectionModel(true /* multiple */); + + // Angular Material Tree Configuration end ------------ /** * Get offices and holiday template from `Resolver`. @@ -43,12 +79,173 @@ export class CreateHolidayComponent implements OnInit { private dateUtils: Dates, private organizationService: OrganizationService, private settings: SettingsService, - private router: Router ) { + private router: Router, + private _database: ChecklistDatabase, + private createHoliday: CreateHoliday ) { this.route.data.subscribe((data: { offices: any, holidayTemplate: any }) => { this.officesData = data.offices; this.repaymentSchedulingTypes = data.holidayTemplate; + // Constructs trie everytime data changes + this.constructOfficeHierarchy(); + // Updates data in the CheckListDatabase + _database.initialize(this.officesTrie); + }); + this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren); + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + // Listens for changes in CheckListDatabase + this._database.dataChange.subscribe((data) => { + this.dataSource.data = data; }); } + constructOfficeHierarchy() { + const trie = {}; + // Iterates over all the offices + this.officesData.forEach((office: any) => { + this.officesDict[office.id] = office; + + let hierarchy = office.hierarchy.split('.'); + + // Removes empty strings + hierarchy = hierarchy.filter((stage: any) => { + return stage.length > 0; + }); + + // Temporary variable to store current object + + let root = trie; + + if (hierarchy.length === 0) { + root[office.id] = {}; + } else { + // Sets root as the first object + root = trie[Object.keys(trie)[0]]; + // Construction of hierarchy via Depth First Search + hierarchy.forEach((stage: string) => { + if (!(stage in root)) { + root[stage] = {}; + } + root = root[stage]; + }); + } + }); + + // Sets leaf offices to null as per the requirement of Angular Material + this.createHoliday.setEmptyObjectsToNull(trie); + + this.officesTrie = trie; + } + + // angular material tree config start ---------------------- + + getLevel = (node: TodoItemFlatNode) => node.level; + + isExpandable = (node: TodoItemFlatNode) => node.expandable; + + getChildren = (node: TodoItemNode): TodoItemNode[] => node.children; + + hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable; + + hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === ''; + + /** + * Transformer to convert nested node to flat node. Record the nodes in maps for later use. + */ + transformer = (node: TodoItemNode, level: number) => { + const existingNode = this.nestedNodeMap.get(node); + const flatNode = existingNode && existingNode.item === node.item ? existingNode : new TodoItemFlatNode(); + flatNode.item = node.item; + flatNode.level = level; + flatNode.expandable = !!node.children?.length; + this.flatNodeMap.set(flatNode, node); + this.nestedNodeMap.set(node, flatNode); + return flatNode; + } + + /** Whether all the descendants of the node are selected. */ + descendantsAllSelected(node: TodoItemFlatNode): boolean { + return this.checklistSelection.isSelected(node); + } + + /** Whether part of the descendants are selected */ + descendantsPartiallySelected(node: TodoItemFlatNode): boolean { + const descendants = this.treeControl.getDescendants(node); + const result = descendants.some((child) => this.checklistSelection.isSelected(child)); + return result && !this.descendantsAllSelected(node); + } + + setSelectedOffices() { + this.holidayForm.patchValue({ + offices: this.checklistSelection.selected.map((item) => item.item), + }); + } + + /** Toggle the to-do item selection. Select/deselect all the descendants node */ + todoItemSelectionToggle(node: TodoItemFlatNode): void { + this.checklistSelection.toggle(node); + const descendants = this.treeControl.getDescendants(node); + this.checklistSelection.isSelected(node) + ? this.checklistSelection.select(...descendants) + : this.checklistSelection.deselect(...descendants); + + // Force update for the parent + descendants.forEach((child) => this.checklistSelection.isSelected(child)); + this.setSelectedOffices(); + } + + /** Toggle a leaf to-do item selection. Check all the parents to see if they changed */ + todoLeafItemSelectionToggle(node: TodoItemFlatNode): void { + this.checklistSelection.toggle(node); + this.setSelectedOffices(); + } + + /* Checks all the parents when a leaf node is selected/unselected */ + checkAllParentsSelection(node: TodoItemFlatNode): void { + let parent: TodoItemFlatNode | null = this.getParentNode(node); + while (parent) { + this.checkRootNodeSelection(parent); + parent = this.getParentNode(parent); + } + } + + /** Check root node checked state and change it accordingly */ + checkRootNodeSelection(node: TodoItemFlatNode): void { + const nodeSelected = this.checklistSelection.isSelected(node); + const descendants = this.treeControl.getDescendants(node); + const descAllSelected = + descendants.length > 0 && + descendants.every((child) => { + return this.checklistSelection.isSelected(child); + }); + if (nodeSelected && !descAllSelected) { + this.checklistSelection.deselect(node); + } else if (!nodeSelected && descAllSelected) { + this.checklistSelection.select(node); + } + } + + /* Get the parent node of a node */ + getParentNode(node: TodoItemFlatNode): TodoItemFlatNode | null { + const currentLevel = this.getLevel(node); + + if (currentLevel < 1) { + return null; + } + + const startIndex = this.treeControl.dataNodes.indexOf(node) - 1; + + for (let i = startIndex; i >= 0; i--) { + const currentNode = this.treeControl.dataNodes[i]; + + if (this.getLevel(currentNode) < currentLevel) { + return currentNode; + } + } + return null; + } + + // angular material tree config end ---------------------- ngOnInit() { this.setHolidayForm(); @@ -91,20 +288,20 @@ export class CreateHolidayComponent implements OnInit { const locale = this.settings.language.code; const prevFromDate: Date = this.holidayForm.value.fromDate; const prevToDate: Date = this.holidayForm.value.toDate; - if (holidayFormData.closureDate instanceof Date) { - holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat); - } - if (holidayFormData.closureDate instanceof Date) { - holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat); - } + holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat); + holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat); if (this.holidayForm.contains('repaymentsRescheduledTo')) { const prevRepaymentsRescheduledTo: Date = this.holidayForm.value.repaymentsRescheduledTo; holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDate(prevRepaymentsRescheduledTo, dateFormat); } + const offices = this.holidayForm.value.offices.map((office: string) => { + return {'officeId': Number.parseInt(office, 10)}; + }); const data = { ...holidayFormData, dateFormat, - locale + locale, + offices }; this.organizationService.createHoliday(data).subscribe((response: any) => { this.router.navigate(['../', response.resourceId], { relativeTo: this.route }); diff --git a/src/app/organization/holidays/create-holiday/create-holiday.service.ts b/src/app/organization/holidays/create-holiday/create-holiday.service.ts new file mode 100644 index 000000000..6cebbd079 --- /dev/null +++ b/src/app/organization/holidays/create-holiday/create-holiday.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CreateHoliday { + setEmptyObjectsToNull(root: any) { + Object.keys(root).forEach((key) => { + if (Object.keys(root[key]).length === 0) { + root[key] = null; + } else { + this.setEmptyObjectsToNull(root[key]); + } + }); + } + +} diff --git a/src/app/organization/holidays/create-holiday/todo-flat-item.class.ts b/src/app/organization/holidays/create-holiday/todo-flat-item.class.ts new file mode 100644 index 000000000..3b2e458b0 --- /dev/null +++ b/src/app/organization/holidays/create-holiday/todo-flat-item.class.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class TodoItemFlatNode { + item: string; + level: number; + expandable: boolean; + } diff --git a/src/app/organization/holidays/create-holiday/todo-item.class.ts b/src/app/organization/holidays/create-holiday/todo-item.class.ts new file mode 100644 index 000000000..882836471 --- /dev/null +++ b/src/app/organization/holidays/create-holiday/todo-item.class.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class TodoItemNode { + children: TodoItemNode[]; + item: string; +} diff --git a/src/app/organization/organization.module.ts b/src/app/organization/organization.module.ts index 92215dc2a..2ac73521c 100644 --- a/src/app/organization/organization.module.ts +++ b/src/app/organization/organization.module.ts @@ -70,6 +70,14 @@ import { StandingInstructionsHistoryComponent } from './standing-instructions-hi import { FundMappingComponent } from './fund-mapping/fund-mapping.component'; import { CreateHolidayComponent } from './holidays/create-holiday/create-holiday.component'; + +/** Custom Service */ +import { TodoItemNode } from './holidays/create-holiday/todo-item.class'; +import { TodoItemFlatNode } from './holidays/create-holiday/todo-flat-item.class'; +import { ChecklistDatabase } from './holidays/create-holiday/checklist-db.class'; +import { CreateHoliday } from './holidays/create-holiday/create-holiday.service'; + + /** * Organization Module * @@ -144,6 +152,12 @@ import { CreateHolidayComponent } from './holidays/create-holiday/create-holiday StandingInstructionsHistoryComponent, FundMappingComponent, CreateHolidayComponent, + ], + providers: [ + CreateHoliday, + ChecklistDatabase, + TodoItemFlatNode, + TodoItemNode ] }) export class OrganizationModule { }