]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
LP1959048: manual ng lint fixes
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / volcopy / vol-edit.component.ts
1 import {Component, OnInit, ViewChild, Input, Renderer2, Output, EventEmitter} from '@angular/core';
2 import {tap} from 'rxjs/operators';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {NetService} from '@eg/core/net.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {VolCopyContext, HoldingsTreeNode} from './volcopy';
9 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
10 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
11 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
12 import {VolCopyService} from './volcopy.service';
13
14 @Component({
15   selector: 'eg-vol-edit',
16   templateUrl: 'vol-edit.component.html',
17   styleUrls: ['vol-edit.component.css']
18 })
19
20
21 export class VolEditComponent implements OnInit {
22
23     @Input() context: VolCopyContext;
24
25     // There are 10 columns in the editor form.  Set the flex values
26     // here so they don't have to be hard-coded and repeated in the
27     // markup.  Changing a flex value here will propagate to all
28     // rows in the form.  Column numbers are 1-based.
29     flexSettings: {[column: number]: number} = {
30         1: 2, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1, 11: 1};
31
32     // Since visibility of some columns is configurable, we need a
33     // map of configured column name to column index.
34     flexColMap = {
35         3: 'classification',
36         4: 'prefix',
37         6: 'suffix',
38         9: 'copy_number_vc',
39         10: 'copy_part'
40     };
41
42     // If a column is specified as the expand field, its flex value
43     // will magically grow.
44     expand: number;
45
46     batchVolClass: ComboboxEntry;
47     batchVolPrefix: ComboboxEntry;
48     batchVolSuffix: ComboboxEntry;
49     batchVolLabel: ComboboxEntry;
50
51     autoBarcodeInProgress = false;
52
53     deleteVolCount: number = null;
54     deleteCopyCount: number = null;
55
56     // When adding multiple vols via add-many popover.
57     addVolCount: number = null;
58
59     // When adding multiple copies via add-many popover.
60     addCopyCount: number = null;
61
62     recordVolLabels: string[] = [];
63
64     @ViewChild('confirmDelVol', {static: false})
65         confirmDelVol: ConfirmDialogComponent;
66
67     @ViewChild('confirmDelCopy', {static: false})
68         confirmDelCopy: ConfirmDialogComponent;
69
70     // Emitted when the save-ability of this form changes.
71     @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
72
73     constructor(
74         private renderer: Renderer2,
75         private idl: IdlService,
76         private org: OrgService,
77         private pcrud: PcrudService,
78         private net: NetService,
79         private auth: AuthService,
80         private holdings: HoldingsService,
81         public  volcopy: VolCopyService
82     ) {}
83
84     ngOnInit() {
85
86         this.deleteVolCount = null;
87         this.deleteCopyCount = null;
88
89         this.volcopy.genBarcodesRequested.subscribe(() => this.generateBarcodes());
90
91         this.volcopy.fetchRecordVolLabels(this.context.recordId)
92         .then(labels => this.recordVolLabels = labels)
93         .then(_ => this.volcopy.fetchBibParts(this.context.getRecordIds()))
94         .then(_ => this.addStubCopies())
95         // It's possible the loaded data is not strictly allowed,
96         // e.g. empty string call number labels
97         .then(_ => this.emitSaveChange(true));
98     }
99
100     copyStatLabel(copy: IdlObject): string {
101         if (copy) {
102             const statId = copy.status();
103             if (statId in this.volcopy.copyStatuses) {
104                 return this.volcopy.copyStatuses[statId].name();
105             }
106         }
107         return '';
108     }
109
110     recordHasParts(bibId: number): boolean {
111         return this.volcopy.bibParts[bibId] &&
112             this.volcopy.bibParts[bibId].length > 0;
113     }
114
115     // Column width (flex:x) for column by column number.
116     flexAt(column: number): number {
117         if (!this.displayColumn(this.flexColMap[column])) {
118             // Hidden columsn are still present, but they do not
119             // flex and the contain no data to display
120             return 0;
121         }
122         let value = this.flexSettings[column];
123         if (this.expand === column) { value = value * 3; }
124         return value;
125     }
126
127     addVol(org: IdlObject) {
128         if (!org) { return; }
129         const orgNode = this.context.findOrCreateOrgNode(org.id());
130         this.createVols(orgNode, 1);
131         this.context.sortHoldings();
132     }
133
134     // This only removes copies that were created during the
135     // current editing session and have not yet been saved in the DB.
136     deleteCopies(volNode: HoldingsTreeNode, count: number) {
137         for (let i = 0;  i < count; i++) {
138             const copyNode = volNode.children[volNode.children.length - 1];
139             if (copyNode && copyNode.target.isnew()) {
140                 volNode.children.pop();
141             } else {
142                 break;
143             }
144         }
145     }
146
147     createCopies(volNode: HoldingsTreeNode, count: number) {
148         const copies = [];
149         for (let i = 0; i < count; i++) {
150
151             // Our context assumes copies are fleshed with volumes
152             const vol = volNode.target;
153             const copy = this.volcopy.createStubCopy(vol);
154             copy.call_number(vol);
155             this.context.findOrCreateCopyNode(copy);
156             copies.push(copy);
157         }
158
159         this.volcopy.setCopyStatus(copies);
160     }
161
162     createCopiesFromPopover(volNode: HoldingsTreeNode, popover: any) {
163         this.createCopies(volNode, this.addCopyCount);
164         popover.close();
165         this.addCopyCount = null;
166     }
167
168     createVolsFromPopover(orgNode: HoldingsTreeNode, popover: any) {
169         this.createVols(orgNode, this.addVolCount);
170         popover.close();
171         this.addVolCount = null;
172     }
173
174     createVols(orgNode: HoldingsTreeNode, count: number) {
175         const vols = [];
176         const copies = [];
177         for (let i = 0; i < count; i++) {
178
179             // This will vivify the volNode if needed.
180             const vol = this.volcopy.createStubVol(
181                 this.context.recordId, orgNode.target.id());
182
183             vols.push(vol);
184
185             // Our context assumes copies are fleshed with volumes
186             const copy = this.volcopy.createStubCopy(vol);
187             copy.call_number(vol);
188             copies.push(copy);
189             this.context.findOrCreateCopyNode(copy);
190         }
191
192         this.volcopy.setCopyStatus(copies);
193         this.volcopy.setVolClassLabels(vols);
194     }
195
196     // This only removes vols that were created during the
197     // current editing session and have not yet been saved in the DB.
198     deleteVols(orgNode: HoldingsTreeNode, count: number) {
199         for (let i = 0;  i < count; i++) {
200             const volNode = orgNode.children[orgNode.children.length - 1];
201             if (volNode && volNode.target.isnew()) {
202                 orgNode.children.pop();
203             } else {
204                 break;
205             }
206         }
207     }
208
209     // When editing existing vols, be sure each has at least one copy.
210     addStubCopies(volNode?: HoldingsTreeNode) {
211         const nodes = volNode ? [volNode] : this.context.volNodes();
212
213         const copies = [];
214         nodes.forEach(vNode => {
215             if (vNode.children.length === 0) {
216                 const vol = vNode.target;
217                 const copy = this.volcopy.createStubCopy(vol);
218                 copy.call_number(vol);
219                 copies.push(copy);
220                 this.context.findOrCreateCopyNode(copy);
221             }
222         });
223
224         this.volcopy.setCopyStatus(copies);
225     }
226
227     applyVolValue(vol: IdlObject, key: string, value: any) {
228
229         if (value === null && (key === 'prefix' || key === 'suffix')) {
230             // -1 is the empty prefix/suffix value.
231             value = -1;
232         }
233
234         if (vol[key]() !== value) {
235             vol[key](value);
236             vol.ischanged(true);
237         }
238
239         this.emitSaveChange();
240     }
241
242     applyCopyValue(copy: IdlObject, key: string, value: any) {
243         if (copy[key]() !== value) {
244             copy[key](value);
245             copy.ischanged(true);
246         }
247     }
248
249     copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
250         const copy = copyNode.target;
251         const part = copyNode.target.parts()[0];
252
253         if (entry) {
254
255             let newPart;
256             if (entry.freetext) {
257                 newPart = this.idl.create('bmp');
258                 newPart.isnew(true);
259                 newPart.record(copy.call_number().record());
260                 newPart.label(entry.label);
261
262             } else {
263
264                 newPart =
265                     this.volcopy.bibParts[copy.call_number().record()]
266                     .filter(p => p.id() === entry.id)[0];
267
268                 // Nothing to change?
269                 if (part && part.id() === newPart.id()) { return; }
270             }
271
272             copy.parts([newPart]);
273             copy.ischanged(true);
274
275         } else if (part) { // Part map no longer needed.
276
277             copy.parts([]);
278             copy.ischanged(true);
279         }
280     }
281
282     batchVolApply() {
283         this.context.volNodes().forEach(volNode => {
284             const vol = volNode.target;
285             if (this.batchVolClass) {
286                 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
287             }
288             if (this.batchVolPrefix) {
289                 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
290             }
291             if (this.batchVolSuffix) {
292                 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
293             }
294             if (this.batchVolLabel) {
295                 // Use label; could be freetext.
296                 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
297             }
298         });
299     }
300
301     // Focus and select the next editable barcode.
302     selectNextBarcode(id: number, previous?: boolean) {
303         let found = false;
304         let nextId: number = null;
305         let firstId: number = null;
306
307         let copies = this.context.copyList();
308         if (previous) { copies = copies.reverse(); }
309
310         // Find the ID of the next item.  If this is the last item,
311         // loop back to the first item.
312         copies.forEach(copy => {
313             if (nextId !== null) { return; }
314
315             // In case we have to loop back to the first copy.
316             if (firstId === null && this.barcodeCanChange(copy)) {
317                 firstId = copy.id();
318             }
319
320             if (found) {
321                 if (nextId === null && this.barcodeCanChange(copy)) {
322                     nextId = copy.id();
323                 }
324             } else if (copy.id() === id) {
325                 found = true;
326             }
327         });
328
329         this.renderer.selectRootElement(
330                 '#barcode-input-' + (nextId || firstId)).select();
331     }
332
333     barcodeCanChange(copy: IdlObject): boolean {
334         return !this.volcopy.copyStatIsMagic(copy.status());
335     }
336
337     generateBarcodes() {
338         this.autoBarcodeInProgress = true;
339
340         // Autogen only replaces barcodes for items which are in
341         // certain statuses.
342         const copies = this.context.copyList()
343         .filter((copy, idx) => {
344             // During autogen we do not replace the first item,
345             // so it's status is not relevant.
346             return idx === 0 || this.barcodeCanChange(copy);
347         });
348
349         if (copies.length > 1) { // seed barcode will always be present
350             this.proceedWithAutogen(copies)
351             .then(_ => this.autoBarcodeInProgress = false);
352         }
353     }
354
355     proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
356
357         const seedBarcode: string = copyList[0].barcode();
358         copyList.shift(); // Avoid replacing the seed barcode
359
360         const count = copyList.length;
361
362         return this.net.request('open-ils.cat',
363             'open-ils.cat.item.barcode.autogen',
364             this.auth.token(), seedBarcode, count, {
365                 checkdigit: this.volcopy.defaults.values.use_checkdigit,
366                 skip_dupes: true
367             }
368         ).pipe(tap(barcodes => {
369
370             copyList.forEach(copy => {
371                 if (copy.barcode() !== barcodes[0]) {
372                     copy.barcode(barcodes[0]);
373                     copy.ischanged(true);
374                 }
375                 barcodes.shift();
376             });
377
378         })).toPromise();
379     }
380
381     barcodeChanged(copy: IdlObject, barcode: string) {
382
383         if (barcode) {
384             // Scrub leading/trailing spaces from barcodes
385             barcode = barcode.trim();
386             copy.barcode(barcode);
387         }
388
389         copy.ischanged(true);
390         copy._dupe_barcode = false;
391
392         if (!barcode) {
393             this.emitSaveChange();
394             return;
395         }
396
397         if (!this.autoBarcodeInProgress) {
398             // Manual barcode entry requires dupe check
399
400             copy._dupe_barcode = false;
401             this.pcrud.search('acp', {
402                 deleted: 'f',
403                 barcode: barcode,
404                 id: {'!=': copy.id()}
405             }).subscribe(
406                 resp => {
407                     if (resp) { copy._dupe_barcode = true; }
408                 },
409                 err => {},
410                 () => this.emitSaveChange()
411             );
412         }
413     }
414
415     deleteCopy(copyNode: HoldingsTreeNode) {
416
417         if (copyNode.target.isnew()) {
418             // Confirmation not required when deleting brand new copies.
419             this.deleteOneCopy(copyNode);
420             return;
421         }
422
423         this.deleteCopyCount = 1;
424         this.confirmDelCopy.open().toPromise().then(confirmed => {
425             if (confirmed) { this.deleteOneCopy(copyNode); }
426         });
427     }
428
429     deleteOneCopy(copyNode: HoldingsTreeNode) {
430         const targetCopy = copyNode.target;
431
432         const orgNodes = this.context.orgNodes();
433         for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
434             const orgNode = orgNodes[orgIdx];
435
436             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
437                 const volNode = orgNode.children[volIdx];
438
439                 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
440                     const copy = volNode.children[copyIdx].target;
441
442                     if (copy.id() === targetCopy.id()) {
443                         volNode.children.splice(copyIdx, 1);
444                         if (!copy.isnew()) {
445                             copy.isdeleted(true);
446                             this.context.copiesToDelete.push(copy);
447                         }
448
449                         if (volNode.children.length === 0) {
450                             // When removing the last copy, add a stub copy.
451                             this.addStubCopies();
452                         }
453
454                         return;
455                     }
456                 }
457             }
458         }
459     }
460
461
462     deleteVol(volNode: HoldingsTreeNode) {
463
464         if (volNode.target.isnew()) {
465             // Confirmation not required when deleting brand new vols.
466             this.deleteOneVol(volNode);
467             return;
468         }
469
470         this.deleteVolCount = 1;
471         this.deleteCopyCount = volNode.children.length;
472
473         this.confirmDelVol.open().toPromise().then(confirmed => {
474             if (confirmed) { this.deleteOneVol(volNode); }
475         });
476     }
477
478     deleteOneVol(volNode: HoldingsTreeNode) {
479
480         let deleteVolIdx = null;
481         const targetVol = volNode.target;
482
483         // FOR loops allow for early exit
484         const orgNodes = this.context.orgNodes();
485         for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
486             const orgNode = orgNodes[orgIdx];
487
488             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
489                 const vol = orgNode.children[volIdx].target;
490
491                 if (vol.id() === targetVol.id()) {
492                     deleteVolIdx = volIdx;
493
494                     if (vol.isnew()) {
495                         // New volumes, which can only have new copies
496                         // may simply be removed from the holdings
497                         // tree to delete them.
498                         break;
499                     }
500
501                     // Mark volume and attached copies as deleted
502                     // and track for later deletion.
503                     targetVol.isdeleted(true);
504                     this.context.volsToDelete.push(targetVol);
505
506                     // When deleting vols, no need to delete the linked
507                     // copies.  They'll be force deleted via the API.
508                 }
509
510                 if (deleteVolIdx !== null) { break; }
511             }
512
513             if (deleteVolIdx !== null) {
514                 orgNode.children.splice(deleteVolIdx, 1);
515                 break;
516             }
517         }
518     }
519
520     editVolOwner(volNode: HoldingsTreeNode, org: IdlObject) {
521         if (!org) { return; }
522
523         const orgId = org.id();
524         const vol = volNode.target;
525
526         vol.owning_lib(orgId);
527         vol.ischanged(true);
528
529         // Move the vol node away from its previous org node and append
530         // it to the children list of the target node.
531         let targetOrgNode: HoldingsTreeNode;
532         this.context.orgNodes().forEach(orgNode => {
533
534             if (orgNode.target.id() === orgId) {
535                 targetOrgNode = orgNode;
536                 return;
537             }
538
539             orgNode.children.forEach((vNode, volIdx) => {
540                 if (vol.id() === vNode.target.id()) {
541                     orgNode.children.splice(volIdx, 1);
542                 }
543             });
544         });
545
546         if (!targetOrgNode) {
547             targetOrgNode = this.context.findOrCreateOrgNode(orgId);
548         }
549
550         targetOrgNode.children.push(volNode);
551
552         // If configured to do so, also update the circ_lib for any
553         // copies linked to this call number in this edit session.
554         if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
555             volNode.children.forEach(copyNode => {
556                 const copy = copyNode.target;
557                 if (copy.circ_lib() !== orgId) {
558                     copy.circ_lib(orgId);
559                     copy.ischanged(true);
560                 }
561             });
562         }
563
564         this.emitSaveChange();
565     }
566
567
568     displayColumn(field: string): boolean {
569         return this.volcopy.defaults.hidden[field] !== true;
570     }
571
572     canSave(): boolean {
573
574         const copies = this.context.copyList();
575
576         const badCopies = copies.filter(copy => {
577             return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
578         }).length > 0;
579
580         if (badCopies) { return false; }
581
582         const badVols = this.context.volNodes().filter(volNode => {
583             const vol = volNode.target;
584             return !(
585                 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
586             );
587         }).length > 0;
588
589         return !badVols;
590     }
591
592     // Called any time a change occurs that could affect the
593     // save-ability of the form.
594     emitSaveChange(initialLoad?: boolean) {
595         const saveable = this.canSave();
596
597         // Avoid emitting a save change event when this was called
598         // during page load and the resulting data is saveable.
599         if (initialLoad && saveable) { return; }
600
601         setTimeout(() => {
602             this.canSaveChange.emit(saveable);
603         });
604     }
605
606     // Given a DOM ID, focus the element after a 0 timeout.
607     focusElement(domId: string) {
608         setTimeout(() => {
609             const node = document.getElementById(domId);
610             if (node) { node.focus(); }
611         });
612     }
613
614
615     toggleBatchVisibility() {
616         this.volcopy.defaults.visible.batch_actions =
617             !this.volcopy.defaults.visible.batch_actions;
618         this.volcopy.saveDefaults();
619     }
620 }
621