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 {
24 @ViewChild('editString', { static: true }) editString: StringComponent;
25 @ViewChild('errorString', { static: true }) errorString: StringComponent;
26 @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
29 private idl: IdlService,
30 private org: OrgService,
31 private auth: AuthService,
32 private pcrud: PcrudService,
33 private strings: StringService,
34 private toast: ToastService
39 this.loadAouTree(this.org.root().id());
42 tabChanged(evt: NgbTabChangeEvent) {
43 const tab = evt.nextId;
44 // stubbing out in case we need it.
47 orgSaved(orgId: number | IdlObject) {
50 if (orgId) { // new org created, focus it.
51 id = typeof orgId === 'object' ? orgId.id() : orgId;
52 } else if (this.currentOrg()) {
53 id = this.currentOrg().id();
56 this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
63 loadAouTree(selectNodeId?: number): Promise<any> {
65 const flesh = ['children', 'ou_type', 'hours_of_operation'];
67 return this.pcrud.search('aou', {parent_ou : null},
68 {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
70 ).toPromise().then(tree => {
71 this.ingestAouTree(tree);
72 if (!selectNodeId) { selectNodeId = this.org.root().id(); }
74 const node = this.tree.findNode(selectNodeId);
76 this.tree.selectNode(node);
78 // Subtract out the menu bar plus a bit more.
79 this.winHeight = window.innerHeight * 0.8;
83 // Translate the org unt type tree into a structure EgTree can use.
84 ingestAouTree(aouTree) {
86 const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
87 if (!orgNode) { return; }
89 if (!orgNode.hours_of_operation()) {
90 this.generateHours(orgNode);
93 const treeNode = new TreeNode({
95 label: orgNode.name(),
96 callerData: {orgUnit: orgNode},
100 // Apply the compiled label asynchronously
101 this.strings.interpolate(
102 'admin.server.org_unit.treenode', {org: orgNode}
103 ).then(label => treeNode.label = label);
105 // Tree node labels are "name -- shortname". Sorting
106 // by name suffices and bypasses the need the wait
107 // for all of the labels to interpolate.
109 .sort((a, b) => a.name() < b.name() ? -1 : 1)
110 .forEach(childNode =>
111 treeNode.children.push(handleNode(childNode))
117 const rootNode = handleNode(aouTree, true);
118 this.tree = new Tree(rootNode);
121 nodeClicked($event: any) {
122 this.selected = $event;
125 generateHours(org: IdlObject) {
126 const hours = this.idl.create('aouhoo');
130 [0, 1, 2, 3, 4, 5, 6].forEach(dow => {
131 this.hours(dow, 'open', '09:00:00', hours);
132 this.hours(dow, 'close', '17:00:00', hours);
135 org.hours_of_operation(hours);
138 // if a 'value' is passed, it will be applied to the optional
139 // hours-of-operation object, otherwise the hours on the currently
140 // selected org unit.
141 hours(dow: number, which: 'open' | 'close', value?: string, hoo?: IdlObject): string {
142 if (!hoo && !this.selected) { return null; }
144 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
147 hours[`dow_${dow}_${which}`](value);
148 hours.ischanged(true);
151 return hours[`dow_${dow}_${which}`]();
154 isClosed(dow: number): boolean {
156 this.hours(dow, 'open') === '00:00:00' &&
157 this.hours(dow, 'close') === '00:00:00'
161 closedOn(dow: number) {
162 this.hours(dow, 'open', '00:00:00');
163 this.hours(dow, 'close', '00:00:00');
167 const org = this.currentOrg();
168 const hours = org.hours_of_operation();
169 this.pcrud.autoApply(hours).subscribe(
171 console.debug('Hours saved ', result);
172 this.editString.current()
173 .then(msg => this.toast.success(msg));
176 this.errorString.current()
177 .then(msg => this.toast.danger(msg));
179 () => this.loadAouTree(this.selected.id)
184 const hours = this.currentOrg().hours_of_operation();
185 const promise = hours.isnew() ? Promise.resolve() :
186 this.pcrud.remove(hours).toPromise();
188 promise.then(_ => this.generateHours(this.currentOrg()));
191 currentOrg(): IdlObject {
192 return this.selected ? this.selected.callerData.orgUnit : null;
195 orgHasChildren(): boolean {
196 const org = this.currentOrg();
197 return (org && org.children().length > 0);
200 postUpdate(message: StringComponent) {
201 // Modifying org unit types means refetching the org unit
202 // data normally fetched on page load, since it includes
203 // org unit type data.
204 this.org.fetchOrgs().then(() =>
205 message.current().then(str => this.toast.success(str)));
209 this.delConfirm.open().subscribe(confirmed => {
210 if (!confirmed) { return; }
212 const org = this.selected.callerData.orgUnit;
214 this.pcrud.remove(org).subscribe(
217 this.errorString.current()
218 .then(str => this.toast.danger(str));
221 // Avoid updating until we know the entire
222 // pcrud action/transaction completed.
223 // After removal, select the parent org if available
224 // otherwise the root org.
225 const orgId = org.parent_ou() ?
226 org.parent_ou() : this.org.root().id();
227 this.loadAouTree(orgId).then(_ =>
228 this.postUpdate(this.editString));
234 orgTypeOptions(): ComboboxEntry[] {
235 let ouType = this.currentOrg().ou_type();
237 if (typeof ouType === 'number') {
238 // May not be fleshed for new org units
239 ouType = this.org.typeMap()[ouType];
241 const curDepth = ouType.depth();
243 return this.org.typeList()
244 .filter(type_ => type_.depth() === curDepth)
245 .map(type_ => ({id: type_.id(), label: type_.name()}));
248 orgChildTypes(): IdlObject[] {
249 let ouType = this.currentOrg().ou_type();
251 if (typeof ouType === 'number') {
252 // May not be fleshed for new org units
253 ouType = this.org.typeMap()[ouType];
256 const depth = ouType.depth();
257 return this.org.typeList()
258 .filter(type_ => type_.depth() === depth + 1);
262 const parentTreeNode = this.selected;
263 const parentOrg = this.currentOrg();
264 const newType = this.orgChildTypes()[0];
266 const org = this.idl.create('aou');
268 org.parent_ou(parentOrg.id());
269 org.ou_type(newType.id());
272 // Create a dummy, detached org node to keep the UI happy.
273 this.selected = new TreeNode({
276 callerData: {orgUnit: org}
280 addressChanged(thing: any) {
281 // Reload to pick up org unit address changes.
282 this.orgSaved(this.currentOrg().id());