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' | 'note', 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 getNote(dow: number, hoo?: IdlObject) {
162 if (!hoo && !this.selected) { return null; }
164 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
166 return hours['dow_' + dow + '_note']();
169 setNote(dow: number, value?: string, hoo?: IdlObject) {
171 if (!hoo && !this.selected) { return null; }
173 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
175 hours['dow_' + dow + '_note'](value);
176 hours.ischanged(true);
178 return hours['dow_' + dow + '_note']();
181 note(dow: number, which: 'note', value?: string, hoo?: IdlObject) {
182 if (!hoo && !this.selected) { return null; }
184 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
186 hours[`dow_${dow}_${which}`]("");
187 hours.ischanged(true);
188 } else if (value != hours[`dow_${dow}_${which}`]()) {
189 hours[`dow_${dow}_${which}`](value);
190 hours.ischanged(true);
192 return hours[`dow_${dow}_${which}`]();
195 closedOn(dow: number) {
196 this.hours(dow, 'open', '00:00:00');
197 this.hours(dow, 'close', '00:00:00');
201 const org = this.currentOrg();
202 const hours = org.hours_of_operation();
203 this.pcrud.autoApply(hours).subscribe(
205 console.debug('Hours saved ', result);
206 this.editString.current()
207 .then(msg => this.toast.success(msg));
210 this.errorString.current()
211 .then(msg => this.toast.danger(msg));
213 () => this.loadAouTree(this.selected.id)
218 const hours = this.currentOrg().hours_of_operation();
219 const promise = hours.isnew() ? Promise.resolve() :
220 this.pcrud.remove(hours).toPromise();
222 promise.then(_ => this.generateHours(this.currentOrg()));
225 currentOrg(): IdlObject {
226 return this.selected ? this.selected.callerData.orgUnit : null;
229 orgHasChildren(): boolean {
230 const org = this.currentOrg();
231 return (org && org.children().length > 0);
234 postUpdate(message: StringComponent) {
235 // Modifying org unit types means refetching the org unit
236 // data normally fetched on page load, since it includes
237 // org unit type data.
238 this.org.fetchOrgs().then(() =>
239 message.current().then(str => this.toast.success(str)));
243 this.delConfirm.open().subscribe(confirmed => {
244 if (!confirmed) { return; }
246 const org = this.selected.callerData.orgUnit;
248 this.pcrud.remove(org).subscribe(
251 this.errorString.current()
252 .then(str => this.toast.danger(str));
255 // Avoid updating until we know the entire
256 // pcrud action/transaction completed.
257 // After removal, select the parent org if available
258 // otherwise the root org.
259 const orgId = org.parent_ou() ?
260 org.parent_ou() : this.org.root().id();
261 this.loadAouTree(orgId).then(_ =>
262 this.postUpdate(this.editString));
268 orgTypeOptions(): ComboboxEntry[] {
269 let ouType = this.currentOrg().ou_type();
271 if (typeof ouType === 'number') {
272 // May not be fleshed for new org units
273 ouType = this.org.typeMap()[ouType];
275 const curDepth = ouType.depth();
277 return this.org.typeList()
278 .filter(type_ => type_.depth() === curDepth)
279 .map(type_ => ({id: type_.id(), label: type_.name()}));
282 orgChildTypes(): IdlObject[] {
283 let ouType = this.currentOrg().ou_type();
285 if (typeof ouType === 'number') {
286 // May not be fleshed for new org units
287 ouType = this.org.typeMap()[ouType];
290 const depth = ouType.depth();
291 return this.org.typeList()
292 .filter(type_ => type_.depth() === depth + 1);
296 const parentTreeNode = this.selected;
297 const parentOrg = this.currentOrg();
298 const newType = this.orgChildTypes()[0];
300 const org = this.idl.create('aou');
302 org.parent_ou(parentOrg.id());
303 org.ou_type(newType.id());
306 // Create a dummy, detached org node to keep the UI happy.
307 this.selected = new TreeNode({
310 callerData: {orgUnit: org}
314 addressChanged(thing: any) {
315 // Reload to pick up org unit address changes.
316 this.orgSaved(this.currentOrg().id());