lp1396764 - Hourse of Operation Note Field
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / org-unit.component.ts
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';
14
15 @Component({
16     templateUrl: './org-unit.component.html'
17 })
18 export class OrgUnitComponent implements OnInit {
19
20     tree: Tree;
21     selected: TreeNode;
22     winHeight = 500;
23
24     @ViewChild('editString', { static: true }) editString: StringComponent;
25     @ViewChild('errorString', { static: true }) errorString: StringComponent;
26     @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
27
28     constructor(
29         private idl: IdlService,
30         private org: OrgService,
31         private auth: AuthService,
32         private pcrud: PcrudService,
33         private strings: StringService,
34         private toast: ToastService
35     ) {}
36
37
38     ngOnInit() {
39         this.loadAouTree(this.org.root().id());
40     }
41
42     tabChanged(evt: NgbTabChangeEvent) {
43         const tab = evt.nextId;
44         // stubbing out in case we need it.
45     }
46
47     orgSaved(orgId: number | IdlObject) {
48         let id;
49
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();
54         }
55
56         this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
57     }
58
59     orgDeleted() {
60         this.loadAouTree();
61     }
62
63     loadAouTree(selectNodeId?: number): Promise<any> {
64
65         const flesh = ['children', 'ou_type', 'hours_of_operation'];
66
67         return this.pcrud.search('aou', {parent_ou : null},
68             {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
69
70         ).toPromise().then(tree => {
71             this.ingestAouTree(tree);
72             if (!selectNodeId) { selectNodeId = this.org.root().id(); }
73
74             const node = this.tree.findNode(selectNodeId);
75             this.selected = node;
76             this.tree.selectNode(node);
77
78             // Subtract out the menu bar plus a bit more.
79             this.winHeight = window.innerHeight * 0.8;
80         });
81     }
82
83     // Translate the org unt type tree into a structure EgTree can use.
84     ingestAouTree(aouTree) {
85
86         const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
87             if (!orgNode) { return; }
88
89             if (!orgNode.hours_of_operation()) {
90                 this.generateHours(orgNode);
91             }
92
93             const treeNode = new TreeNode({
94                 id: orgNode.id(),
95                 label: orgNode.name(),
96                 callerData: {orgUnit: orgNode},
97                 expanded: expand
98             });
99
100             // Apply the compiled label asynchronously
101             this.strings.interpolate(
102                 'admin.server.org_unit.treenode', {org: orgNode}
103             ).then(label => treeNode.label = label);
104
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.
108             orgNode.children()
109             .sort((a, b) => a.name() < b.name() ? -1 : 1)
110             .forEach(childNode =>
111                 treeNode.children.push(handleNode(childNode))
112             );
113
114             return treeNode;
115         };
116
117         const rootNode = handleNode(aouTree, true);
118         this.tree = new Tree(rootNode);
119     }
120
121     nodeClicked($event: any) {
122         this.selected = $event;
123     }
124
125     generateHours(org: IdlObject) {
126         const hours = this.idl.create('aouhoo');
127         hours.id(org.id());
128         hours.isnew(true);
129
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);
133         });
134
135         org.hours_of_operation(hours);
136     }
137
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; }
143
144         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
145
146         if (value) {
147             hours[`dow_${dow}_${which}`](value);
148             hours.ischanged(true);
149         }
150
151         return hours[`dow_${dow}_${which}`]();
152     }
153
154     isClosed(dow: number): boolean {
155         return (
156             this.hours(dow, 'open') === '00:00:00' &&
157             this.hours(dow, 'close') === '00:00:00'
158         );
159     }
160     
161     getNote(dow: number, hoo?: IdlObject) {
162         if (!hoo && !this.selected) { return null; }
163
164         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
165
166         return hours['dow_' + dow + '_note']();
167     }
168     
169     setNote(dow: number, value?: string, hoo?: IdlObject) {
170         console.log(value);
171         if (!hoo && !this.selected) { return null; }
172
173         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
174
175         hours['dow_' + dow + '_note'](value);
176         hours.ischanged(true);
177
178         return hours['dow_' + dow + '_note']();
179     }
180
181     note(dow: number, which: 'note', value?: string, hoo?: IdlObject) {
182          if (!hoo && !this.selected) { return null; }
183
184         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
185         if (!value) {
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);
191         }
192         return hours[`dow_${dow}_${which}`]();
193     }
194
195     closedOn(dow: number) {
196         this.hours(dow, 'open', '00:00:00');
197         this.hours(dow, 'close', '00:00:00');
198     }
199
200     saveHours() {
201         const org = this.currentOrg();
202         const hours = org.hours_of_operation();
203         this.pcrud.autoApply(hours).subscribe(
204             result => {
205                 console.debug('Hours saved ', result);
206                 this.editString.current()
207                     .then(msg => this.toast.success(msg));
208             },
209             error => {
210                 this.errorString.current()
211                     .then(msg => this.toast.danger(msg));
212             },
213             () => this.loadAouTree(this.selected.id)
214         );
215     }
216
217     deleteHours() {
218         const hours = this.currentOrg().hours_of_operation();
219         const promise = hours.isnew() ? Promise.resolve() :
220             this.pcrud.remove(hours).toPromise();
221
222         promise.then(_ => this.generateHours(this.currentOrg()));
223     }
224
225     currentOrg(): IdlObject {
226         return this.selected ? this.selected.callerData.orgUnit : null;
227     }
228
229     orgHasChildren(): boolean {
230         const org = this.currentOrg();
231         return (org && org.children().length > 0);
232     }
233
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)));
240     }
241
242     remove() {
243         this.delConfirm.open().subscribe(confirmed => {
244             if (!confirmed) { return; }
245
246             const org = this.selected.callerData.orgUnit;
247
248             this.pcrud.remove(org).subscribe(
249                 ok2 => {},
250                 err => {
251                     this.errorString.current()
252                       .then(str => this.toast.danger(str));
253                 },
254                 ()  => {
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));
263                 }
264             );
265         });
266     }
267
268     orgTypeOptions(): ComboboxEntry[] {
269         let ouType = this.currentOrg().ou_type();
270
271         if (typeof ouType === 'number') {
272             // May not be fleshed for new org units
273             ouType = this.org.typeMap()[ouType];
274         }
275         const curDepth = ouType.depth();
276
277         return this.org.typeList()
278             .filter(type_ => type_.depth() === curDepth)
279             .map(type_ => ({id: type_.id(), label: type_.name()}));
280     }
281
282     orgChildTypes(): IdlObject[] {
283         let ouType = this.currentOrg().ou_type();
284
285         if (typeof ouType === 'number') {
286             // May not be fleshed for new org units
287             ouType = this.org.typeMap()[ouType];
288         }
289
290         const depth = ouType.depth();
291         return this.org.typeList()
292             .filter(type_ => type_.depth() === depth + 1);
293     }
294
295     addChild() {
296         const parentTreeNode = this.selected;
297         const parentOrg = this.currentOrg();
298         const newType = this.orgChildTypes()[0];
299
300         const org = this.idl.create('aou');
301         org.isnew(true);
302         org.parent_ou(parentOrg.id());
303         org.ou_type(newType.id());
304         org.children([]);
305
306         // Create a dummy, detached org node to keep the UI happy.
307         this.selected = new TreeNode({
308             id: org.id(),
309             label: org.name(),
310             callerData: {orgUnit: org}
311         });
312     }
313
314     addressChanged(thing: any) {
315         // Reload to pick up org unit address changes.
316         this.orgSaved(this.currentOrg().id());
317     }
318 }
319