]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.ts
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / org-unit.component.ts
1 /* eslint-disable eqeqeq, max-len, no-magic-numbers */
2 import {Component, ViewChild, OnInit} from '@angular/core';
3 import {Tree, TreeNode} from '@eg/share/tree/tree';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
6 import {OrgService} from '@eg/core/org.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {ToastService} from '@eg/share/toast/toast.service';
10 import {StringComponent} from '@eg/share/string/string.component';
11 import {StringService} from '@eg/share/string/string.service';
12 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
13 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
14 import {PermService} from '@eg/core/perm.service';
15
16 @Component({
17     templateUrl: './org-unit.component.html',
18     styleUrls: [ './org-unit.component.css' ],
19 })
20 export class OrgUnitComponent implements OnInit {
21
22     tree: Tree;
23     selected: TreeNode;
24     orgUnitTab: string;
25
26     hasClosedDatePerms: boolean;
27
28     @ViewChild('editString', { static: true }) editString: StringComponent;
29     @ViewChild('errorString', { static: true }) errorString: StringComponent;
30     @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
31
32     constructor(
33         private idl: IdlService,
34         private org: OrgService,
35         private auth: AuthService,
36         private pcrud: PcrudService,
37         private strings: StringService,
38         private toast: ToastService,
39         private perm: PermService,
40     ) {}
41
42
43     ngOnInit() {
44         this.loadAouTree(this.org.root().id());
45
46         // Check once on init if user could be linked to closed date editor (don't want them to land on a page that does nothing and think it's broken)
47         const neededClosedDatesPerms = ['actor.org_unit.closed_date.create',
48             'actor.org_unit.closed_date.update',
49             'actor.org_unit.closed_date.delete'];
50
51         this.perm.hasWorkPermAt(neededClosedDatesPerms, true).then((perm) => {
52             // Set true once if they have every permission they need to change closed dates
53             this.hasClosedDatePerms = neededClosedDatesPerms.every(element => {
54                 return perm[element].length > 0;
55             });
56         });
57
58     }
59
60     navChanged(evt: NgbNavChangeEvent) {
61         const tab = evt.nextId;
62         // stubbing out in case we need it.
63     }
64
65     orgSaved(orgId: number | IdlObject) {
66         let id;
67
68         if (orgId) { // new org created, focus it.
69             id = typeof orgId === 'object' ? orgId.id() : orgId;
70         } else if (this.currentOrg()) {
71             id = this.currentOrg().id();
72         }
73
74         this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
75     }
76
77     orgDeleted() {
78         this.loadAouTree();
79     }
80
81     loadAouTree(selectNodeId?: number): Promise<any> {
82
83         const flesh = ['children', 'ou_type', 'hours_of_operation'];
84
85         return this.pcrud.search('aou', {parent_ou : null},
86             {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
87
88         ).toPromise().then(tree => {
89             this.ingestAouTree(tree);
90             if (!selectNodeId) { selectNodeId = this.org.root().id(); }
91
92             const node = this.tree.findNode(selectNodeId);
93             this.selected = node;
94             this.tree.selectNode(node);
95         });
96     }
97
98     // Translate the org unt type tree into a structure EgTree can use.
99     ingestAouTree(aouTree) {
100
101         const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
102             if (!orgNode) { return; }
103
104             if (!orgNode.hours_of_operation()) {
105                 this.generateHours(orgNode);
106             }
107
108             const treeNode = new TreeNode({
109                 id: orgNode.id(),
110                 label: orgNode.name(),
111                 callerData: {orgUnit: orgNode},
112                 expanded: expand
113             });
114
115             // Apply the compiled label asynchronously
116             this.strings.interpolate(
117                 'admin.server.org_unit.treenode', {org: orgNode}
118             ).then(label => treeNode.label = label);
119
120             // Tree node labels are "name -- shortname".  Sorting
121             // by name suffices and bypasses the need the wait
122             // for all of the labels to interpolate.
123             orgNode.children()
124                 .sort((a, b) => a.name() < b.name() ? -1 : 1)
125                 .forEach(childNode =>
126                     treeNode.children.push(handleNode(childNode))
127                 );
128
129             return treeNode;
130         };
131
132         const rootNode = handleNode(aouTree, true);
133         this.tree = new Tree(rootNode);
134     }
135
136     nodeClicked($event: any) {
137         this.selected = $event;
138     }
139
140     generateHours(org: IdlObject) {
141         const hours = this.idl.create('aouhoo');
142         hours.id(org.id());
143         hours.isnew(true);
144
145         [0, 1, 2, 3, 4, 5, 6].forEach(dow => {
146             this.hours(dow, 'open', '09:00:00', hours);
147             this.hours(dow, 'close', '17:00:00', hours);
148         });
149
150         org.hours_of_operation(hours);
151     }
152
153     // if a 'value' is passed, it will be applied to the optional
154     // hours-of-operation object, otherwise the hours on the currently
155     // selected org unit.
156     hours(dow: number, which: 'open' | 'close' | 'note', value?: string, hoo?: IdlObject): string {
157         if (!hoo && !this.selected) { return null; }
158
159         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
160
161         if (value) {
162             hours[`dow_${dow}_${which}`](value);
163             hours.ischanged(true);
164         }
165
166         return hours[`dow_${dow}_${which}`]();
167     }
168
169     isClosed(dow: number): boolean {
170         return (
171             this.hours(dow, 'open') === '00:00:00' &&
172             this.hours(dow, 'close') === '00:00:00'
173         );
174     }
175
176     // Is the org closed every day of the week?
177     allClosed(): boolean{
178         return [0, 1, 2, 3, 4, 5, 6].every(dow => this.isClosed(dow));
179     }
180
181     getNote(dow: number, hoo?: IdlObject) {
182         if (!hoo && !this.selected) { return null; }
183
184         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
185
186         return hours['dow_' + dow + '_note']();
187     }
188
189     setNote(dow: number, value?: string, hoo?: IdlObject) {
190         console.log(value);
191         if (!hoo && !this.selected) { return null; }
192
193         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
194
195         hours['dow_' + dow + '_note'](value);
196         hours.ischanged(true);
197
198         return hours['dow_' + dow + '_note']();
199     }
200
201     note(dow: number, which: 'note', value?: string, hoo?: IdlObject) {
202         if (!hoo && !this.selected) { return null; }
203
204         const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
205         if (!value) {
206             hours[`dow_${dow}_${which}`]('');
207             hours.ischanged(true);
208         } else if (value != hours[`dow_${dow}_${which}`]()) {
209             hours[`dow_${dow}_${which}`](value);
210             hours.ischanged(true);
211         }
212         return hours[`dow_${dow}_${which}`]();
213     }
214
215     closedOn(dow: number) {
216         this.hours(dow, 'open', '00:00:00');
217         this.hours(dow, 'close', '00:00:00');
218     }
219
220     saveHours() {
221         const org = this.currentOrg();
222         const hours = org.hours_of_operation();
223         this.pcrud.autoApply(hours).subscribe(
224             result => {
225                 console.debug('Hours saved ', result);
226                 this.editString.current()
227                     .then(msg => this.toast.success(msg));
228             },
229             (error: unknown) => {
230                 this.errorString.current()
231                     .then(msg => this.toast.danger(msg));
232             },
233             () => this.loadAouTree(this.selected.id)
234         );
235     }
236
237     deleteHours() {
238         const hours = this.currentOrg().hours_of_operation();
239         const promise = hours.isnew() ? Promise.resolve() :
240             this.pcrud.remove(hours).toPromise();
241
242         promise.then(_ => this.generateHours(this.currentOrg()));
243     }
244
245     currentOrg(): IdlObject {
246         return this.selected ? this.selected.callerData.orgUnit : null;
247     }
248
249     orgHasChildren(): boolean {
250         const org = this.currentOrg();
251         return (org && org.children().length > 0);
252     }
253
254     postUpdate(message: StringComponent) {
255         // Modifying org unit types means refetching the org unit
256         // data normally fetched on page load, since it includes
257         // org unit type data.
258         this.org.fetchOrgs().then(() =>
259             message.current().then(str => this.toast.success(str)));
260     }
261
262     remove() {
263         this.delConfirm.open().subscribe(confirmed => {
264             if (!confirmed) { return; }
265
266             const org = this.selected.callerData.orgUnit;
267
268             // eslint-disable-next-line rxjs/no-nested-subscribe
269             this.pcrud.remove(org).subscribe(
270                 ok2 => {},
271                 (err: unknown) => {
272                     this.errorString.current()
273                         .then(str => this.toast.danger(str));
274                 },
275                 ()  => {
276                     // Avoid updating until we know the entire
277                     // pcrud action/transaction completed.
278                     // After removal, select the parent org if available
279                     // otherwise the root org.
280                     const orgId = org.parent_ou() ?
281                         org.parent_ou() : this.org.root().id();
282                     this.loadAouTree(orgId).then(_ =>
283                         this.postUpdate(this.editString));
284                 }
285             );
286         });
287     }
288
289     orgTypeOptions(): ComboboxEntry[] {
290         let ouType = this.currentOrg().ou_type();
291
292         if (typeof ouType === 'number') {
293             // May not be fleshed for new org units
294             ouType = this.org.typeMap()[ouType];
295         }
296         const curDepth = ouType.depth();
297
298         return this.org.typeList()
299             .filter(type_ => type_.depth() === curDepth)
300             .map(type_ => ({id: type_.id(), label: type_.name()}));
301     }
302
303     orgChildTypes(): IdlObject[] {
304         let ouType = this.currentOrg().ou_type();
305
306         if (typeof ouType === 'number') {
307             // May not be fleshed for new org units
308             ouType = this.org.typeMap()[ouType];
309         }
310
311         const depth = ouType.depth();
312         return this.org.typeList()
313             .filter(type_ => type_.depth() === depth + 1);
314     }
315
316     addChild() {
317         const parentTreeNode = this.selected;
318         const parentOrg = this.currentOrg();
319         const newType = this.orgChildTypes()[0];
320
321         const org = this.idl.create('aou');
322         org.isnew(true);
323         org.parent_ou(parentOrg.id());
324         org.ou_type(newType.id());
325         org.children([]);
326
327         // Create a dummy, detached org node to keep the UI happy.
328         this.selected = new TreeNode({
329             id: org.id(),
330             label: org.name(),
331             callerData: {orgUnit: org}
332         });
333     }
334
335     addressChanged(thing: any) {
336         // Reload to pick up org unit address changes.
337         this.orgSaved(this.currentOrg().id());
338     }
339 }
340