Merge pull request #1516 from ramvr1256/holidayview
feat: Holiday office multiselect replaced with TreeView
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { OfficeItemNode } from './office-item.class';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChecklistDatabase {
|
||||
|
||||
|
||||
// rxjs functionality to update DOM via subscribe
|
||||
dataChange = new BehaviorSubject<OfficeItemNode[]>([]);
|
||||
get data(): OfficeItemNode[] {
|
||||
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 officeItemNodes
|
||||
buildFileTree(obj: { [key: string]: any }, level: number): OfficeItemNode[] {
|
||||
return Object.keys(obj).reduce<OfficeItemNode[]>((accumulator, key) => {
|
||||
const value = obj[key];
|
||||
const node = new OfficeItemNode();
|
||||
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);
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
@@ -63,18 +63,27 @@
|
||||
<input matInput formControlName="description">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- TODO: Replace multi-select by custom angular material tree -->
|
||||
<mat-form-field>
|
||||
<mat-label>Applicable Offices</mat-label>
|
||||
<mat-select required formControlName="offices" multiple>
|
||||
<mat-option *ngFor="let office of officesData" [value]="{ officeId: office.id }">
|
||||
{{ office.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="holidayForm.controls.offices.hasError('required')">
|
||||
Offices is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<label>Select applicable offices</label>
|
||||
|
||||
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
|
||||
<button mat-icon-button disabled></button>
|
||||
<mat-checkbox class="checklist-leaf-node"
|
||||
[checked]="checklistSelection.isSelected(node)"
|
||||
(change)="officeLeafItemSelectionToggle(node);">{{officesDict[node.item].name}}</mat-checkbox>
|
||||
</mat-tree-node>
|
||||
|
||||
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
|
||||
<button type="button" mat-icon-button matTreeNodeToggle
|
||||
[attr.aria-label]="'toggle ' + node.filename">
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-checkbox [checked]="descendantsAllSelected(node)"
|
||||
(change)="officeItemSelectionToggle(node)">{{ officesDict[node.item].name }}</mat-checkbox>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
/** 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 { OfficeItemNode } from './office-item.class';
|
||||
import { OfficeItemFlatNode } from './office-flat-item.class';
|
||||
import { ChecklistDatabase } from './checklist-db.class';
|
||||
import { CreateHoliday } from './create-holiday.service';
|
||||
|
||||
/**
|
||||
* Create Holiday component.
|
||||
* TODO: Develop a custom angular checkbox tree and replace offices select.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-create-holiday',
|
||||
@@ -29,6 +35,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<OfficeItemFlatNode, OfficeItemNode>();
|
||||
|
||||
/** Map from nested node to flattened node. This helps us to keep the same object for selection */
|
||||
nestedNodeMap = new Map<OfficeItemNode, OfficeItemFlatNode>();
|
||||
|
||||
/** A selected parent node to be inserted */
|
||||
selectedParent: OfficeItemFlatNode | null = null;
|
||||
|
||||
/** The new item's name */
|
||||
newItemName = '';
|
||||
|
||||
treeControl: FlatTreeControl<OfficeItemFlatNode>;
|
||||
|
||||
treeFlattener: MatTreeFlattener<OfficeItemNode, OfficeItemFlatNode>;
|
||||
|
||||
dataSource: MatTreeFlatDataSource<OfficeItemNode, OfficeItemFlatNode>;
|
||||
|
||||
/** The selection for checklist */
|
||||
checklistSelection = new SelectionModel<OfficeItemFlatNode>(true /* multiple */);
|
||||
|
||||
// Angular Material Tree Configuration end ------------
|
||||
|
||||
/**
|
||||
* Get offices and holiday template from `Resolver`.
|
||||
@@ -43,12 +78,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<OfficeItemFlatNode>(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: OfficeItemFlatNode) => node.level;
|
||||
|
||||
isExpandable = (node: OfficeItemFlatNode) => node.expandable;
|
||||
|
||||
getChildren = (node: OfficeItemNode): OfficeItemNode[] => node.children;
|
||||
|
||||
hasChild = (_: number, _nodeData: OfficeItemFlatNode) => _nodeData.expandable;
|
||||
|
||||
hasNoContent = (_: number, _nodeData: OfficeItemFlatNode) => _nodeData.item === '';
|
||||
|
||||
/**
|
||||
* Transformer to convert nested node to flat node. Record the nodes in maps for later use.
|
||||
*/
|
||||
transformer = (node: OfficeItemNode, level: number) => {
|
||||
const existingNode = this.nestedNodeMap.get(node);
|
||||
const flatNode = existingNode && existingNode.item === node.item ? existingNode : new OfficeItemFlatNode();
|
||||
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: OfficeItemFlatNode): boolean {
|
||||
return this.checklistSelection.isSelected(node);
|
||||
}
|
||||
|
||||
/** Whether part of the descendants are selected */
|
||||
descendantsPartiallySelected(node: OfficeItemFlatNode): 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 */
|
||||
officeItemSelectionToggle(node: OfficeItemFlatNode): 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 */
|
||||
officeLeafItemSelectionToggle(node: OfficeItemFlatNode): void {
|
||||
this.checklistSelection.toggle(node);
|
||||
this.setSelectedOffices();
|
||||
}
|
||||
|
||||
/* Checks all the parents when a leaf node is selected/unselected */
|
||||
checkAllParentsSelection(node: OfficeItemFlatNode): void {
|
||||
let parent: OfficeItemFlatNode | null = this.getParentNode(node);
|
||||
while (parent) {
|
||||
this.checkRootNodeSelection(parent);
|
||||
parent = this.getParentNode(parent);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check root node checked state and change it accordingly */
|
||||
checkRootNodeSelection(node: OfficeItemFlatNode): 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: OfficeItemFlatNode): OfficeItemFlatNode | 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 +287,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 });
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfficeItemFlatNode {
|
||||
item: string;
|
||||
level: number;
|
||||
expandable: boolean;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfficeItemNode {
|
||||
children: OfficeItemNode[];
|
||||
item: string;
|
||||
}
|
||||
@@ -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 { OfficeItemNode } from './holidays/create-holiday/office-item.class';
|
||||
import { OfficeItemFlatNode } from './holidays/create-holiday/office-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,
|
||||
OfficeItemFlatNode,
|
||||
OfficeItemNode
|
||||
]
|
||||
})
|
||||
export class OrganizationModule { }
|
||||
|
||||
Reference in New Issue
Block a user