1 import {Component, Input, ViewChild, OnInit} from '@angular/core';
2 import {Tree, TreeNode} from '@eg/share/tree/tree';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
5 import {OrgService} from '@eg/core/org.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {StringComponent} from '@eg/share/string/string.component';
10 import {StringService} from '@eg/share/string/string.service';
11 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
12 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
13 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
16 templateUrl: './org-unit.component.html'
18 export class OrgUnitComponent implements OnInit {
22 @ViewChild('editString', { static: true }) editString: StringComponent;
23 @ViewChild('errorString', { static: true }) errorString: StringComponent;
24 @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
27 private idl: IdlService,
28 private org: OrgService,
29 private auth: AuthService,
30 private pcrud: PcrudService,
31 private strings: StringService,
32 private toast: ToastService
37 this.loadAouTree(this.org.root().id());
40 tabChanged(evt: NgbTabChangeEvent) {
41 const tab = evt.nextId;
42 // stubbing out in case we need it.
45 orgSaved(orgId: number | IdlObject) {
48 if (orgId) { // new org created, focus it.
49 id = typeof orgId === 'object' ? orgId.id() : orgId;
50 } else if (this.currentOrg()) {
51 id = this.currentOrg().id();
54 this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
61 loadAouTree(selectNodeId?: number): Promise<any> {
63 const flesh = ['children', 'ou_type', 'hours_of_operation'];
65 return this.pcrud.search('aou', {parent_ou : null},
66 {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
68 ).toPromise().then(tree => {
69 this.ingestAouTree(tree);
70 if (!selectNodeId) { selectNodeId = this.org.root().id(); }
72 const node = this.tree.findNode(selectNodeId);
74 this.tree.selectNode(node);
78 // Translate the org unt type tree into a structure EgTree can use.
79 ingestAouTree(aouTree) {
81 const handleNode = (orgNode: IdlObject): TreeNode => {
82 if (!orgNode) { return; }
84 if (!orgNode.hours_of_operation()) {
85 this.generateHours(orgNode);
88 const treeNode = new TreeNode({
90 label: orgNode.name(),
91 callerData: {orgUnit: orgNode}
94 // Apply the compiled label asynchronously
95 this.strings.interpolate(
96 'admin.server.org_unit.treenode', {org: orgNode}
97 ).then(label => treeNode.label = label);
99 // Tree node labels are "name -- shortname". Sorting
100 // by name suffices and bypasses the need the wait
101 // for all of the labels to interpolate.
103 .sort((a, b) => a.name() < b.name() ? -1 : 1)
104 .forEach(childNode =>
105 treeNode.children.push(handleNode(childNode))
111 const rootNode = handleNode(aouTree);
112 this.tree = new Tree(rootNode);
115 nodeClicked($event: any) {
116 this.selected = $event;
119 generateHours(org: IdlObject) {
120 const hours = this.idl.create('aouhoo');
124 [0, 1, 2, 3, 4, 5, 6].forEach(dow => {
125 this.hours(dow, 'open', '09:00:00', hours);
126 this.hours(dow, 'close', '17:00:00', hours);
129 org.hours_of_operation(hours);
132 // if a 'value' is passed, it will be applied to the optional
133 // hours-of-operation object, otherwise the hours on the currently
134 // selected org unit.
135 hours(dow: number, which: 'open' | 'close', value?: string, hoo?: IdlObject): string {
136 if (!hoo && !this.selected) { return null; }
138 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
141 hours[`dow_${dow}_${which}`](value);
142 hours.ischanged(true);
145 return hours[`dow_${dow}_${which}`]();
148 isClosed(dow: number): boolean {
150 this.hours(dow, 'open') === '00:00:00' &&
151 this.hours(dow, 'close') === '00:00:00'
155 closedOn(dow: number) {
156 this.hours(dow, 'open', '00:00:00');
157 this.hours(dow, 'close', '00:00:00');
161 const org = this.currentOrg();
162 const hours = org.hours_of_operation();
163 this.pcrud.autoApply(hours).subscribe(
165 console.debug('Hours saved ', result);
166 this.editString.current()
167 .then(msg => this.toast.success(msg));
170 this.errorString.current()
171 .then(msg => this.toast.danger(msg));
173 () => this.loadAouTree(this.selected.id)
178 const hours = this.currentOrg().hours_of_operation();
179 const promise = hours.isnew() ? Promise.resolve() :
180 this.pcrud.remove(hours).toPromise();
182 promise.then(_ => this.generateHours(this.currentOrg()));
185 currentOrg(): IdlObject {
186 return this.selected ? this.selected.callerData.orgUnit : null;
189 orgHasChildren(): boolean {
190 const org = this.currentOrg();
191 return (org && org.children().length > 0);
194 postUpdate(message: StringComponent) {
195 // Modifying org unit types means refetching the org unit
196 // data normally fetched on page load, since it includes
197 // org unit type data.
198 this.org.fetchOrgs().then(() =>
199 message.current().then(str => this.toast.success(str)));
203 this.delConfirm.open().subscribe(confirmed => {
204 if (!confirmed) { return; }
206 const org = this.selected.callerData.orgUnit;
208 this.pcrud.remove(org).subscribe(
211 this.errorString.current()
212 .then(str => this.toast.danger(str));
215 // Avoid updating until we know the entire
216 // pcrud action/transaction completed.
217 // After removal, select the parent org if available
218 // otherwise the root org.
219 const orgId = org.parent_ou() ?
220 org.parent_ou() : this.org.root().id();
221 this.loadAouTree(orgId).then(_ =>
222 this.postUpdate(this.editString));
228 orgTypeOptions(): ComboboxEntry[] {
229 let ouType = this.currentOrg().ou_type();
231 if (typeof ouType === 'number') {
232 // May not be fleshed for new org units
233 ouType = this.org.typeMap()[ouType];
235 const curDepth = ouType.depth();
237 return this.org.typeList()
238 .filter(type_ => type_.depth() === curDepth)
239 .map(type_ => ({id: type_.id(), label: type_.name()}));
242 orgChildTypes(): IdlObject[] {
243 let ouType = this.currentOrg().ou_type();
245 if (typeof ouType === 'number') {
246 // May not be fleshed for new org units
247 ouType = this.org.typeMap()[ouType];
250 const depth = ouType.depth();
251 return this.org.typeList()
252 .filter(type_ => type_.depth() === depth + 1);
256 const parentTreeNode = this.selected;
257 const parentOrg = this.currentOrg();
258 const newType = this.orgChildTypes()[0];
260 const org = this.idl.create('aou');
262 org.parent_ou(parentOrg.id());
263 org.ou_type(newType.id());
266 // Create a dummy, detached org node to keep the UI happy.
267 this.selected = new TreeNode({
270 callerData: {orgUnit: org}
274 addressChanged(thing: any) {
275 // Reload to pick up org unit address changes.
276 this.orgSaved(this.currentOrg().id());