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">
|
<input matInput formControlName="description">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- TODO: Replace multi-select by custom angular material tree -->
|
<label>Select applicable offices</label>
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Applicable Offices</mat-label>
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||||
<mat-select required formControlName="offices" multiple>
|
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
|
||||||
<mat-option *ngFor="let office of officesData" [value]="{ officeId: office.id }">
|
<button mat-icon-button disabled></button>
|
||||||
{{ office.name }}
|
<mat-checkbox class="checklist-leaf-node"
|
||||||
</mat-option>
|
[checked]="checklistSelection.isSelected(node)"
|
||||||
</mat-select>
|
(change)="officeLeafItemSelectionToggle(node);">{{officesDict[node.item].name}}</mat-checkbox>
|
||||||
<mat-error *ngIf="holidayForm.controls.offices.hasError('required')">
|
</mat-tree-node>
|
||||||
Offices is <strong>required</strong>
|
|
||||||
</mat-error>
|
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
|
||||||
</mat-form-field>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
/** Angular Imports. */
|
/** 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 { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Dates } from 'app/core/utils/dates';
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
|
||||||
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
|
|
||||||
/** Custom Services. */
|
/** Custom Services. */
|
||||||
import { OrganizationService } from 'app/organization/organization.service';
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
import { SettingsService } from 'app/settings/settings.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.
|
* Create Holiday component.
|
||||||
* TODO: Develop a custom angular checkbox tree and replace offices select.
|
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-create-holiday',
|
selector: 'mifosx-create-holiday',
|
||||||
@@ -29,6 +35,35 @@ export class CreateHolidayComponent implements OnInit {
|
|||||||
minDate = new Date(2000, 0, 1);
|
minDate = new Date(2000, 0, 1);
|
||||||
/** Maximum Date allowed. */
|
/** Maximum Date allowed. */
|
||||||
maxDate = new Date(2100, 0, 1);
|
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`.
|
* Get offices and holiday template from `Resolver`.
|
||||||
@@ -43,12 +78,173 @@ export class CreateHolidayComponent implements OnInit {
|
|||||||
private dateUtils: Dates,
|
private dateUtils: Dates,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private settings: SettingsService,
|
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.route.data.subscribe((data: { offices: any, holidayTemplate: any }) => {
|
||||||
this.officesData = data.offices;
|
this.officesData = data.offices;
|
||||||
this.repaymentSchedulingTypes = data.holidayTemplate;
|
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() {
|
ngOnInit() {
|
||||||
this.setHolidayForm();
|
this.setHolidayForm();
|
||||||
@@ -91,20 +287,20 @@ export class CreateHolidayComponent implements OnInit {
|
|||||||
const locale = this.settings.language.code;
|
const locale = this.settings.language.code;
|
||||||
const prevFromDate: Date = this.holidayForm.value.fromDate;
|
const prevFromDate: Date = this.holidayForm.value.fromDate;
|
||||||
const prevToDate: Date = this.holidayForm.value.toDate;
|
const prevToDate: Date = this.holidayForm.value.toDate;
|
||||||
if (holidayFormData.closureDate instanceof Date) {
|
holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat);
|
||||||
holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat);
|
holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat);
|
||||||
}
|
|
||||||
if (holidayFormData.closureDate instanceof Date) {
|
|
||||||
holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat);
|
|
||||||
}
|
|
||||||
if (this.holidayForm.contains('repaymentsRescheduledTo')) {
|
if (this.holidayForm.contains('repaymentsRescheduledTo')) {
|
||||||
const prevRepaymentsRescheduledTo: Date = this.holidayForm.value.repaymentsRescheduledTo;
|
const prevRepaymentsRescheduledTo: Date = this.holidayForm.value.repaymentsRescheduledTo;
|
||||||
holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDate(prevRepaymentsRescheduledTo, dateFormat);
|
holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDate(prevRepaymentsRescheduledTo, dateFormat);
|
||||||
}
|
}
|
||||||
|
const offices = this.holidayForm.value.offices.map((office: string) => {
|
||||||
|
return {'officeId': Number.parseInt(office, 10)};
|
||||||
|
});
|
||||||
const data = {
|
const data = {
|
||||||
...holidayFormData,
|
...holidayFormData,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
locale
|
locale,
|
||||||
|
offices
|
||||||
};
|
};
|
||||||
this.organizationService.createHoliday(data).subscribe((response: any) => {
|
this.organizationService.createHoliday(data).subscribe((response: any) => {
|
||||||
this.router.navigate(['../', response.resourceId], { relativeTo: this.route });
|
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 { FundMappingComponent } from './fund-mapping/fund-mapping.component';
|
||||||
import { CreateHolidayComponent } from './holidays/create-holiday/create-holiday.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
|
* Organization Module
|
||||||
*
|
*
|
||||||
@@ -144,6 +152,12 @@ import { CreateHolidayComponent } from './holidays/create-holiday/create-holiday
|
|||||||
StandingInstructionsHistoryComponent,
|
StandingInstructionsHistoryComponent,
|
||||||
FundMappingComponent,
|
FundMappingComponent,
|
||||||
CreateHolidayComponent,
|
CreateHolidayComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CreateHoliday,
|
||||||
|
ChecklistDatabase,
|
||||||
|
OfficeItemFlatNode,
|
||||||
|
OfficeItemNode
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class OrganizationModule { }
|
export class OrganizationModule { }
|
||||||
|
|||||||
Reference in New Issue
Block a user