]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / custom-org-unit-trees.component.ts
1 /* eslint-disable no-await-in-loop, no-shadow */
2 import {Component, ViewChild, OnInit} from '@angular/core';
3 import {catchError, firstValueFrom, lastValueFrom, of, take, defaultIfEmpty} from 'rxjs';
4 import {Tree, TreeNode} from '@eg/share/tree/tree';
5 import {IdlService, IdlObject} from '@eg/core/idl.service';
6 import {OrgService} from '@eg/core/org.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 {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
11 import {CustomOrgUnitTreesDialogComponent} from './custom-org-unit-trees-dialog.component';
12
13 @Component({
14     templateUrl: './custom-org-unit-trees.component.html',
15     styleUrls: [ './custom-org-unit-trees.component.css' ],
16 })
17
18 export class CustomOrgUnitTreesComponent implements OnInit {
19
20     tree: Tree;
21     custom_tree: Tree;
22     aouctn_root: IdlObject;
23     tree_type: IdlObject;
24     active = false;
25     selected: TreeNode;
26     custom_selected: TreeNode;
27     orgUnitTab: string;
28     singleNodeSelected = false;
29     multipleNodesSelected = false;
30     noNodesSelected = false;
31
32     @ViewChild('editString', { static: true }) editString: StringComponent;
33     @ViewChild('errorString', { static: true }) errorString: StringComponent;
34     @ViewChild('successString', { static: true }) successString: StringComponent;
35     @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent;
36     @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
37     @ViewChild('moveNodeElsewhereDialog', { static: true })
38         moveNodeElsewhereDialog: CustomOrgUnitTreesDialogComponent;
39
40     constructor(
41         private idl: IdlService,
42         private org: OrgService,
43         private pcrud: PcrudService,
44         // private strings: StringService,
45         private toast: ToastService
46     ) {}
47
48
49     async ngOnInit() {
50         try {
51             await this.loadAouTree(this.org.root().id());
52             await this.loadCustomTree('opac');
53             // console.warn('CustomOrgUnitTreesComponent, this', this);
54         } catch(E) {
55             console.error('caught during ngOnInit',E);
56         }
57     }
58
59     async loadAouTree(selectNodeId?: number): Promise<any> {
60         const flesh = ['children', 'ou_type', 'hours_of_operation'];
61
62         try {
63             const tree = await firstValueFrom(this.pcrud.search('aou', {parent_ou : null},
64                 {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
65             ));
66
67             this.ingestAouTree(tree); // sets this.tree as a side-effect
68             if (!selectNodeId) { selectNodeId = this.org.root().id(); }
69
70             /* const node = this.tree.findNode(selectNodeId);
71             this.selected = node;
72             this.tree.selectNode(node);*/
73
74             return this.tree;
75         } catch (E) {
76             console.warn('caught from pcrud (aou)', E);
77         }
78     }
79
80     async loadCustomTree(purpose: string): Promise<any> {
81         const flesh = ['children', 'org_unit'];
82
83         this.tree_type = await firstValueFrom(
84             this.pcrud.search('aouct', { purpose: purpose })
85                 .pipe(
86                     take(1),
87                     defaultIfEmpty(undefined),
88                     catchError((err: unknown) => {
89                         console.warn('caught from pcrud (aouct): 1', err);
90                         return of(undefined);
91                     })
92                 )
93         );
94
95         let tree_id: number;
96         if (this.tree_type) {
97             tree_id = this.tree_type.id();
98             this.active = this.tree_type.active() === 't';
99         } else {
100             tree_id = null;
101         }
102
103         this.aouctn_root = undefined;
104         if (tree_id) {
105             this.aouctn_root = await firstValueFrom(
106                 this.pcrud.search('aouctn', {tree: tree_id, parent_node: null},
107                     {flesh: -1, flesh_fields: {aouctn: flesh}}, {authoritative: true})
108                     .pipe(
109                         take(1),
110                         defaultIfEmpty(undefined),
111                         catchError((err: unknown) => {
112                             console.warn('phasefx: caught from pcrud (aouctn): 2', err);
113                             return of(undefined);
114                         })
115                     )
116             );
117         } else {
118             this.tree_type = this.idl.create('aouct');
119             this.tree_type.isnew('t');
120             this.tree_type.purpose('opac');
121             this.tree_type.active(this.active ? 't' : 'f');
122         }
123         if (this.aouctn_root) {
124             this.ingestCustomTree(this.aouctn_root); // sets this.custom_tree as a side-effect
125         } else {
126             this.custom_tree = this.tree.clone();
127         }
128         return this.custom_tree;
129     }
130
131     // Translate the org unt type tree into a structure EgTree can use.
132     ingestAouTree(aouTree: IdlObject) {
133
134         const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
135             if (!orgNode) { return; }
136
137             const treeNode = new TreeNode({
138                 id: orgNode.id(),
139                 label: orgNode.name() + '--' + orgNode.shortname(),
140                 callerData: {orgId: orgNode.id()},
141                 expanded: expand
142             });
143
144             // Tree node labels are "name -- shortname".  Sorting
145             // by name suffices and bypasses the need the wait
146             // for all of the labels to interpolate.
147             orgNode.children()
148                 .sort((a: IdlObject, b: IdlObject) => a.name() < b.name() ? -1 : 1)
149                 .forEach((childNode: IdlObject) =>
150                     treeNode.children.push(handleNode(childNode))
151                 );
152
153             return treeNode;
154         };
155
156         const rootNode = handleNode(aouTree, true);
157         this.tree = new Tree(rootNode);
158     }
159
160     ingestCustomTree(aouctnTree: IdlObject) {
161
162         const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
163             if (!orgNode) { return; }
164
165             const treeNode = new TreeNode({
166                 id: orgNode.id(),
167                 label: orgNode.org_unit().name() + '--' + orgNode.org_unit().shortname(),
168                 callerData: {orgId: orgNode.org_unit().id()},
169                 expanded: expand
170             });
171
172             orgNode.children()
173                 .sort((a: IdlObject, b: IdlObject) => a.sibling_order() < b.sibling_order() ? -1 : 1)
174                 .forEach((childNode: IdlObject) =>
175                     treeNode.children.push(handleNode(childNode))
176                 );
177
178             return treeNode;
179         };
180
181         const rootNode = handleNode(aouctnTree, true);
182         this.custom_tree = new Tree(rootNode);
183     }
184
185     nodeClicked($event: any) {
186         // this.selected = $event;
187         // console.log('custom: nodeClicked',typeof $event);
188     }
189
190     custom_nodeClicked($event: any) {
191         // this.custom_selected = $event;
192         // console.log('custom: custom_nodeClicked',typeof $event);
193     }
194
195     nodeChecked($event: any) {
196         // this.selected = $event;
197         // console.log('custom: nodeChecked',typeof $event);
198     }
199
200     custom_nodeChecked($event: any) {
201         // this.custom_selected = $event;
202         // console.log('custom: custom_nodeChecked',typeof $event);
203     }
204
205     isCopyNodesAllowed(): boolean {
206         try {
207             if (!this.tree) {
208                 // console.log('isCopyNodesAllowed: tree not ready', false);
209                 return false;
210             }
211             const sourceNodes = this.tree.selectedNodes();
212             if (sourceNodes.length === 0) {
213                 // console.log('isCopyNodesAllowed: no sourceNodes selected', false);
214                 return false;
215             }
216             const destinationNode = this.custom_tree.selectedNode();
217             if (!destinationNode) {
218                 // console.log('isCopyNodesAllowed: no destinationNode selected', false);
219                 return false;
220             }
221             for (const sourceNode of sourceNodes) {
222                 if (this.custom_tree.findNodesByFieldAndValue('label', sourceNode.label).length > 0) {
223                     // console.log('isCopyNodesAllowed: selected SourceNode already in custom_tree', false);
224                     return false;
225                 }
226                 if (sourceNode === this.tree.rootNode) {
227                     // console.log('isCopyNodesAllowed: rootNode is sacrosanct', false);
228                     return false;
229                 }
230             }
231             // console.log('isCopyNodesAllowed', true);
232             return true;
233         } catch(E) {
234             console.log('isCopyNodesAllowed, error', E);
235             return false;
236         }
237     }
238
239     copyNodes() {
240         // console.log('copyNodes');
241         const sourceNodes = this.tree.selectedNodes();
242         const targetNode = this.custom_tree.selectedNode();
243         if (!this.isCopyNodesAllowed()) {
244             return;
245         }
246         this._copyNodes(sourceNodes, targetNode, false);
247     }
248
249     _copyNodes(sourceNodes: TreeNode[], targetNode: TreeNode, cloneChildren = true) {
250         // console.log('_copyNodes', { sourceNodes: sourceNodes, targetNode: targetNode });
251         const traverseTreeAndCopySourceNodes = (currentNode: TreeNode, targetNode: TreeNode) => {
252             // console.log('traverseTreeAndCopySourceNodes', currentNode.label);
253             if (sourceNodes.map(n => n.label).includes(currentNode.label)) {
254                 // console.log('found a source node, copying',currentNode.label);
255                 const newNode = currentNode.clone();
256                 if (!cloneChildren) {
257                     newNode.children = [];
258                 }
259                 targetNode.children.push(newNode);
260                 targetNode = newNode;
261             }
262
263             for (const childNode of currentNode.children) {
264                 traverseTreeAndCopySourceNodes(childNode, targetNode);
265             }
266         };
267
268         traverseTreeAndCopySourceNodes(this.tree.rootNode, targetNode);
269         this.custom_tree.nodeList(); // re-index
270     }
271
272     isDeleteNodesAllowed(): boolean {
273         try {
274             if (!this.custom_tree) {
275                 // console.log('isDeleteNodesAllowed: custom_tree not ready', false);
276                 return false;
277             }
278             const targetNodes = this.custom_tree.selectedNodes();
279             if (targetNodes.length === 0) {
280                 // console.log('isDeleteNodesAllowed: no targetNodes selected', false);
281                 return false;
282             }
283             for (const targetNode of targetNodes) {
284                 if (targetNode === this.custom_tree.rootNode) {
285                     // console.log('isDeleteNodesAllowed: rootNode is sacrosanct', false);
286                     return false;
287                 }
288             }
289             // console.log('isDeleteNodesAllowed', true);
290             return true;
291         } catch(E) {
292             console.log('isDeleteNodesAllowed, error', E);
293             return false;
294         }
295     }
296
297     deleteNodes(targetNodes: TreeNode[]) {
298         if (! this.isDeleteNodesAllowed()) {
299             return;
300         }
301         if (! window.confirm($localize`Are you sure?`)) {
302             return;
303         }
304
305         // Sort nodes by depth in descending order
306         targetNodes.sort((a, b) => b.depth - a.depth);
307
308         for (const targetNode of targetNodes) {
309             if (targetNode !== this.custom_tree.rootNode) {
310                 // console.log('removing node',targetNode);
311                 this.custom_tree.removeNode(targetNode);
312             }
313         }
314         this.custom_tree.nodeList(); // re-index
315     }
316
317     deleteNode(node: TreeNode) {
318         this.deleteNodes([node]);
319     }
320
321     deleteSelectedNodes() {
322         this.deleteNodes(this.custom_tree.selectedNodes());
323     }
324
325     isMoveNodeUpAllowed(node: TreeNode): boolean {
326         const parentNode = this.custom_tree.findParentNode(node);
327         if (parentNode) {
328             const index = parentNode.children.indexOf(node);
329             if (index === 0) {
330                 return false;
331             }
332         }
333         return true;
334     }
335
336     moveNodeUp(node: TreeNode) {
337         const selectedNode = node || this.custom_tree.selectedNode();
338         if (!this.isMoveNodeUpAllowed(node)) {
339             return;
340         }
341         const parentNode = this.custom_tree.findParentNode(selectedNode);
342         if (parentNode) {
343             const index = parentNode.children.indexOf(selectedNode);
344             if (index > 0) {
345                 // Swap the selected node with its previous sibling.
346                 const temp = parentNode.children[index - 1];
347                 parentNode.children[index - 1] = selectedNode;
348                 parentNode.children[index] = temp;
349                 this.custom_tree.nodeList(); // re-index
350             }
351         }
352     }
353
354     isMoveNodeDownAllowed(node: TreeNode): boolean {
355         const parentNode = this.custom_tree.findParentNode(node);
356         if (parentNode) {
357             const index = parentNode.children.indexOf(node);
358             if (index < parentNode.children.length - 1) {
359                 // great
360             } else {
361                 return false;
362             }
363         }
364         return true;
365     }
366
367     moveNodeDown(node: TreeNode) {
368         if (!this.isMoveNodeDownAllowed(node)) {
369             return;
370         }
371         const parentNode = this.custom_tree.findParentNode(node);
372         if (parentNode) {
373             const index = parentNode.children.indexOf(node);
374             if (index < parentNode.children.length - 1) {
375                 // Swap the selected node with its next sibling.
376                 const temp = parentNode.children[index + 1];
377                 parentNode.children[index + 1] = node;
378                 parentNode.children[index] = temp;
379                 this.custom_tree.nodeList(); // re-index
380             }
381         }
382     }
383
384     isMoveNodeElsewhereAllowed(node: TreeNode): boolean {
385         return node !== this.custom_tree.rootNode;
386     }
387
388     moveNodeElsewhere() {
389         const nodeToMove = this.custom_tree.selectedNode();
390         const selectionTree = this.custom_tree.clone();
391
392         // prune nodeToMove and descendants from destination selection tree
393         const equivalentNode = selectionTree.findNodesByFieldAndValue(
394             'label',nodeToMove.label)[0];
395         selectionTree.removeNode(equivalentNode);
396
397         this.moveNodeElsewhereDialog.customTree = selectionTree;
398         this.moveNodeElsewhereDialog.nodeToMove = nodeToMove;
399
400
401         this.moveNodeElsewhereDialog.open({size: 'lg'}).subscribe(
402             result => {
403                 // console.log('modal result',result);
404                 if (result) {
405                     try {
406                         // Find the equivalent node in custom_tree
407                         const targetNodeInCustomTree = this.custom_tree.findNodesByFieldAndValue(
408                             'label',result.label)[0];
409
410                         // Prevent a node from becoming its own parent.
411                         if (nodeToMove === targetNodeInCustomTree
412                             || this.custom_tree.findParentNode(targetNodeInCustomTree) === nodeToMove) {
413                             return;
414                         }
415
416                         // Remove the selected node from its current parent's children.
417                         // this.custom_tree.removeNode(nodeToMove);
418
419                         // Add the selected node as the last child of the target node in custom_tree.
420                         if (targetNodeInCustomTree) {
421                             this.custom_tree.removeNode(nodeToMove);
422                             // this._copyNodes([nodeToMove], targetNodeInCustomTree);
423                             targetNodeInCustomTree.children.push( nodeToMove );
424                         }
425
426                         // re-index
427                         this.custom_tree.nodeList();
428
429                     } catch(E) {
430                         console.error('moveNodeHere',E);
431                     }
432                 }
433             }
434         );
435     }
436
437     async applyChanges() {
438         // console.log('applyChanges');
439         if (this.active !== (this.tree_type.active() === 't')) {
440             this.tree_type.active(this.active ? 't' : 'f');
441             this.tree_type.ischanged('t');
442         }
443         try {
444             if (this.tree_type.isnew()) {
445                 this.tree_type = await firstValueFrom(this.pcrud.create(this.tree_type));
446             } else if (this.tree_type.ischanged()) {
447                 await firstValueFrom(this.pcrud.update(this.tree_type));
448             }
449             await this.createNewAouctns(this.custom_tree.rootNode);
450             this.successString.current().then(str => this.toast.success(str));
451
452         } catch (error) {
453             console.error('Error applying changes:', error);
454             this.updateFailedString.current().then(str => this.toast.danger(str));
455         }
456     }
457
458     async createNewAouctns(node: TreeNode, parent_id: number = null, order = 0) {
459         // console.log('createNewAouctns for ' + node.label + ' with parent_id = ' + parent_id + ' and order = ' + order, node);
460         // delete the existing custom nodes for the custom tree
461         // TODO: this is what the dojo interface did, but do we really need so much churn?
462         // TODO: we may want to move this to an OpenSRF method so we can wrap the entire
463         //       delete and create into a single transaction
464         if (this.aouctn_root) {
465             if (this.org.get(this.aouctn_root.org_unit()).id() === node.callerData.orgId) {
466                 // console.warn('removing aouctn for org ' + this.org.get(node.callerData.orgId).shortname(), this.aouctn_root);
467                 const result = await lastValueFrom(this.pcrud.remove(this.aouctn_root));
468                 // console.log('remove returned', result);
469                 // console.log('this should have cascaded and deleted all descendants');
470                 // console.log('setting aouctn_root to null');
471                 this.aouctn_root = null;
472             }
473         }
474         let newNode = this.idl.create('aouctn');
475         newNode.isnew('t');
476         newNode.parent_node(parent_id);
477         newNode.sibling_order(order);
478         newNode.org_unit(node.callerData.orgId);
479         newNode.tree(this.tree_type.id());
480         // console.warn('creating aouctn for org ' + this.org.get(node.callerData.orgId).shortname(), newNode);
481
482         // Send the new node to the server and get back the updated node
483         newNode = await firstValueFrom(this.pcrud.create(newNode));
484         // console.log('pcrud.create returned', newNode);
485         if (!this.aouctn_root) {
486             // console.log('setting it to aouctn_root; parent_node =', newNode.parent_node())
487             this.aouctn_root = newNode;
488         }
489
490         // If the original TreeNode has children, create new aouctn's for each child
491         if (node.children && node.children.length > 0) {
492             // console.log('looping through children for ' + this.org.get(newNode.org_unit()).shortname());
493             for (let i = 0; i < node.children.length; i++) {
494                 await this.createNewAouctns(node.children[i], newNode.id(), i);
495             }
496             // console.log('finished with children for ' + this.org.get(newNode.org_unit()).shortname());
497         }
498
499         // console.warn('final version of node', newNode);
500         return newNode;
501     }
502
503 }
504